Add update_globe tool to drive map markers and camera
Browse filesLet the agent highlight destinations on the MapLibre globe via tool calls,
with country centroids and globe state wired through ChatInterface outputs.
- assets/country_centroids.json +1252 -0
- assets/globe.css +15 -0
- assets/globe.js +136 -2
- ui/chat.py +21 -5
- ui/country_coords.py +50 -0
- ui/globe.py +2 -2
- ui/globe_commands.py +133 -0
- ui/inference/respond.py +28 -10
- ui/inference/streaming.py +19 -9
- ui/inference/tool_schemas/__init__.py +2 -0
- ui/inference/tool_schemas/update_globe.py +48 -0
- ui/inference/tools.py +42 -16
assets/country_centroids.json
ADDED
|
@@ -0,0 +1,1252 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"AW": {
|
| 3 |
+
"name": "Aruba",
|
| 4 |
+
"lat": 12.5,
|
| 5 |
+
"lng": -69.96666666
|
| 6 |
+
},
|
| 7 |
+
"AF": {
|
| 8 |
+
"name": "Afghanistan",
|
| 9 |
+
"lat": 33,
|
| 10 |
+
"lng": 65
|
| 11 |
+
},
|
| 12 |
+
"AO": {
|
| 13 |
+
"name": "Angola",
|
| 14 |
+
"lat": -12.5,
|
| 15 |
+
"lng": 18.5
|
| 16 |
+
},
|
| 17 |
+
"AI": {
|
| 18 |
+
"name": "Anguilla",
|
| 19 |
+
"lat": 18.25,
|
| 20 |
+
"lng": -63.16666666
|
| 21 |
+
},
|
| 22 |
+
"AX": {
|
| 23 |
+
"name": "Åland Islands",
|
| 24 |
+
"lat": 60.116667,
|
| 25 |
+
"lng": 19.9
|
| 26 |
+
},
|
| 27 |
+
"AL": {
|
| 28 |
+
"name": "Albania",
|
| 29 |
+
"lat": 41,
|
| 30 |
+
"lng": 20
|
| 31 |
+
},
|
| 32 |
+
"AD": {
|
| 33 |
+
"name": "Andorra",
|
| 34 |
+
"lat": 42.5,
|
| 35 |
+
"lng": 1.5
|
| 36 |
+
},
|
| 37 |
+
"AE": {
|
| 38 |
+
"name": "United Arab Emirates",
|
| 39 |
+
"lat": 24,
|
| 40 |
+
"lng": 54
|
| 41 |
+
},
|
| 42 |
+
"AR": {
|
| 43 |
+
"name": "Argentina",
|
| 44 |
+
"lat": -34,
|
| 45 |
+
"lng": -64
|
| 46 |
+
},
|
| 47 |
+
"AM": {
|
| 48 |
+
"name": "Armenia",
|
| 49 |
+
"lat": 40,
|
| 50 |
+
"lng": 45
|
| 51 |
+
},
|
| 52 |
+
"AS": {
|
| 53 |
+
"name": "American Samoa",
|
| 54 |
+
"lat": -14.33333333,
|
| 55 |
+
"lng": -170
|
| 56 |
+
},
|
| 57 |
+
"AQ": {
|
| 58 |
+
"name": "Antarctica",
|
| 59 |
+
"lat": -90,
|
| 60 |
+
"lng": 0
|
| 61 |
+
},
|
| 62 |
+
"TF": {
|
| 63 |
+
"name": "French Southern and Antarctic Lands",
|
| 64 |
+
"lat": -49.25,
|
| 65 |
+
"lng": 69.167
|
| 66 |
+
},
|
| 67 |
+
"AG": {
|
| 68 |
+
"name": "Antigua and Barbuda",
|
| 69 |
+
"lat": 17.05,
|
| 70 |
+
"lng": -61.8
|
| 71 |
+
},
|
| 72 |
+
"AU": {
|
| 73 |
+
"name": "Australia",
|
| 74 |
+
"lat": -27,
|
| 75 |
+
"lng": 133
|
| 76 |
+
},
|
| 77 |
+
"AT": {
|
| 78 |
+
"name": "Austria",
|
| 79 |
+
"lat": 47.33333333,
|
| 80 |
+
"lng": 13.33333333
|
| 81 |
+
},
|
| 82 |
+
"AZ": {
|
| 83 |
+
"name": "Azerbaijan",
|
| 84 |
+
"lat": 40.5,
|
| 85 |
+
"lng": 47.5
|
| 86 |
+
},
|
| 87 |
+
"BI": {
|
| 88 |
+
"name": "Burundi",
|
| 89 |
+
"lat": -3.5,
|
| 90 |
+
"lng": 30
|
| 91 |
+
},
|
| 92 |
+
"BE": {
|
| 93 |
+
"name": "Belgium",
|
| 94 |
+
"lat": 50.83333333,
|
| 95 |
+
"lng": 4
|
| 96 |
+
},
|
| 97 |
+
"BJ": {
|
| 98 |
+
"name": "Benin",
|
| 99 |
+
"lat": 9.5,
|
| 100 |
+
"lng": 2.25
|
| 101 |
+
},
|
| 102 |
+
"BF": {
|
| 103 |
+
"name": "Burkina Faso",
|
| 104 |
+
"lat": 13,
|
| 105 |
+
"lng": -2
|
| 106 |
+
},
|
| 107 |
+
"BD": {
|
| 108 |
+
"name": "Bangladesh",
|
| 109 |
+
"lat": 24,
|
| 110 |
+
"lng": 90
|
| 111 |
+
},
|
| 112 |
+
"BG": {
|
| 113 |
+
"name": "Bulgaria",
|
| 114 |
+
"lat": 43,
|
| 115 |
+
"lng": 25
|
| 116 |
+
},
|
| 117 |
+
"BH": {
|
| 118 |
+
"name": "Bahrain",
|
| 119 |
+
"lat": 26,
|
| 120 |
+
"lng": 50.55
|
| 121 |
+
},
|
| 122 |
+
"BS": {
|
| 123 |
+
"name": "Bahamas",
|
| 124 |
+
"lat": 24.25,
|
| 125 |
+
"lng": -76
|
| 126 |
+
},
|
| 127 |
+
"BA": {
|
| 128 |
+
"name": "Bosnia and Herzegovina",
|
| 129 |
+
"lat": 44,
|
| 130 |
+
"lng": 18
|
| 131 |
+
},
|
| 132 |
+
"BL": {
|
| 133 |
+
"name": "Saint Barthélemy",
|
| 134 |
+
"lat": 18.5,
|
| 135 |
+
"lng": -63.41666666
|
| 136 |
+
},
|
| 137 |
+
"SH": {
|
| 138 |
+
"name": "Saint Helena, Ascension and Tristan da Cunha",
|
| 139 |
+
"lat": -15.95,
|
| 140 |
+
"lng": -5.72
|
| 141 |
+
},
|
| 142 |
+
"BY": {
|
| 143 |
+
"name": "Belarus",
|
| 144 |
+
"lat": 53,
|
| 145 |
+
"lng": 28
|
| 146 |
+
},
|
| 147 |
+
"BZ": {
|
| 148 |
+
"name": "Belize",
|
| 149 |
+
"lat": 17.25,
|
| 150 |
+
"lng": -88.75
|
| 151 |
+
},
|
| 152 |
+
"BM": {
|
| 153 |
+
"name": "Bermuda",
|
| 154 |
+
"lat": 32.33333333,
|
| 155 |
+
"lng": -64.75
|
| 156 |
+
},
|
| 157 |
+
"BO": {
|
| 158 |
+
"name": "Bolivia",
|
| 159 |
+
"lat": -17,
|
| 160 |
+
"lng": -65
|
| 161 |
+
},
|
| 162 |
+
"BQ": {
|
| 163 |
+
"name": "Caribbean Netherlands",
|
| 164 |
+
"lat": 12.18,
|
| 165 |
+
"lng": -68.25
|
| 166 |
+
},
|
| 167 |
+
"BR": {
|
| 168 |
+
"name": "Brazil",
|
| 169 |
+
"lat": -10,
|
| 170 |
+
"lng": -55
|
| 171 |
+
},
|
| 172 |
+
"BB": {
|
| 173 |
+
"name": "Barbados",
|
| 174 |
+
"lat": 13.16666666,
|
| 175 |
+
"lng": -59.53333333
|
| 176 |
+
},
|
| 177 |
+
"BN": {
|
| 178 |
+
"name": "Brunei",
|
| 179 |
+
"lat": 4.5,
|
| 180 |
+
"lng": 114.66666666
|
| 181 |
+
},
|
| 182 |
+
"BT": {
|
| 183 |
+
"name": "Bhutan",
|
| 184 |
+
"lat": 27.5,
|
| 185 |
+
"lng": 90.5
|
| 186 |
+
},
|
| 187 |
+
"BV": {
|
| 188 |
+
"name": "Bouvet Island",
|
| 189 |
+
"lat": -54.43333333,
|
| 190 |
+
"lng": 3.4
|
| 191 |
+
},
|
| 192 |
+
"BW": {
|
| 193 |
+
"name": "Botswana",
|
| 194 |
+
"lat": -22,
|
| 195 |
+
"lng": 24
|
| 196 |
+
},
|
| 197 |
+
"CF": {
|
| 198 |
+
"name": "Central African Republic",
|
| 199 |
+
"lat": 7,
|
| 200 |
+
"lng": 21
|
| 201 |
+
},
|
| 202 |
+
"CA": {
|
| 203 |
+
"name": "Canada",
|
| 204 |
+
"lat": 60,
|
| 205 |
+
"lng": -95
|
| 206 |
+
},
|
| 207 |
+
"CC": {
|
| 208 |
+
"name": "Cocos (Keeling) Islands",
|
| 209 |
+
"lat": -12.5,
|
| 210 |
+
"lng": 96.83333333
|
| 211 |
+
},
|
| 212 |
+
"CH": {
|
| 213 |
+
"name": "Switzerland",
|
| 214 |
+
"lat": 47,
|
| 215 |
+
"lng": 8
|
| 216 |
+
},
|
| 217 |
+
"CL": {
|
| 218 |
+
"name": "Chile",
|
| 219 |
+
"lat": -30,
|
| 220 |
+
"lng": -71
|
| 221 |
+
},
|
| 222 |
+
"CN": {
|
| 223 |
+
"name": "China",
|
| 224 |
+
"lat": 35,
|
| 225 |
+
"lng": 105
|
| 226 |
+
},
|
| 227 |
+
"CI": {
|
| 228 |
+
"name": "Ivory Coast",
|
| 229 |
+
"lat": 8,
|
| 230 |
+
"lng": -5
|
| 231 |
+
},
|
| 232 |
+
"CM": {
|
| 233 |
+
"name": "Cameroon",
|
| 234 |
+
"lat": 6,
|
| 235 |
+
"lng": 12
|
| 236 |
+
},
|
| 237 |
+
"CD": {
|
| 238 |
+
"name": "DR Congo",
|
| 239 |
+
"lat": 0,
|
| 240 |
+
"lng": 25
|
| 241 |
+
},
|
| 242 |
+
"CG": {
|
| 243 |
+
"name": "Congo",
|
| 244 |
+
"lat": -1,
|
| 245 |
+
"lng": 15
|
| 246 |
+
},
|
| 247 |
+
"CK": {
|
| 248 |
+
"name": "Cook Islands",
|
| 249 |
+
"lat": -21.23333333,
|
| 250 |
+
"lng": -159.76666666
|
| 251 |
+
},
|
| 252 |
+
"CO": {
|
| 253 |
+
"name": "Colombia",
|
| 254 |
+
"lat": 4,
|
| 255 |
+
"lng": -72
|
| 256 |
+
},
|
| 257 |
+
"KM": {
|
| 258 |
+
"name": "Comoros",
|
| 259 |
+
"lat": -12.16666666,
|
| 260 |
+
"lng": 44.25
|
| 261 |
+
},
|
| 262 |
+
"CV": {
|
| 263 |
+
"name": "Cape Verde",
|
| 264 |
+
"lat": 16,
|
| 265 |
+
"lng": -24
|
| 266 |
+
},
|
| 267 |
+
"CR": {
|
| 268 |
+
"name": "Costa Rica",
|
| 269 |
+
"lat": 10,
|
| 270 |
+
"lng": -84
|
| 271 |
+
},
|
| 272 |
+
"CU": {
|
| 273 |
+
"name": "Cuba",
|
| 274 |
+
"lat": 21.5,
|
| 275 |
+
"lng": -80
|
| 276 |
+
},
|
| 277 |
+
"CW": {
|
| 278 |
+
"name": "Curaçao",
|
| 279 |
+
"lat": 12.116667,
|
| 280 |
+
"lng": -68.933333
|
| 281 |
+
},
|
| 282 |
+
"CX": {
|
| 283 |
+
"name": "Christmas Island",
|
| 284 |
+
"lat": -10.5,
|
| 285 |
+
"lng": 105.66666666
|
| 286 |
+
},
|
| 287 |
+
"KY": {
|
| 288 |
+
"name": "Cayman Islands",
|
| 289 |
+
"lat": 19.5,
|
| 290 |
+
"lng": -80.5
|
| 291 |
+
},
|
| 292 |
+
"CY": {
|
| 293 |
+
"name": "Cyprus",
|
| 294 |
+
"lat": 35,
|
| 295 |
+
"lng": 33
|
| 296 |
+
},
|
| 297 |
+
"CZ": {
|
| 298 |
+
"name": "Czechia",
|
| 299 |
+
"lat": 49.75,
|
| 300 |
+
"lng": 15.5
|
| 301 |
+
},
|
| 302 |
+
"DE": {
|
| 303 |
+
"name": "Germany",
|
| 304 |
+
"lat": 51,
|
| 305 |
+
"lng": 9
|
| 306 |
+
},
|
| 307 |
+
"DJ": {
|
| 308 |
+
"name": "Djibouti",
|
| 309 |
+
"lat": 11.5,
|
| 310 |
+
"lng": 43
|
| 311 |
+
},
|
| 312 |
+
"DM": {
|
| 313 |
+
"name": "Dominica",
|
| 314 |
+
"lat": 15.41666666,
|
| 315 |
+
"lng": -61.33333333
|
| 316 |
+
},
|
| 317 |
+
"DK": {
|
| 318 |
+
"name": "Denmark",
|
| 319 |
+
"lat": 56,
|
| 320 |
+
"lng": 10
|
| 321 |
+
},
|
| 322 |
+
"DO": {
|
| 323 |
+
"name": "Dominican Republic",
|
| 324 |
+
"lat": 19,
|
| 325 |
+
"lng": -70.66666666
|
| 326 |
+
},
|
| 327 |
+
"DZ": {
|
| 328 |
+
"name": "Algeria",
|
| 329 |
+
"lat": 28,
|
| 330 |
+
"lng": 3
|
| 331 |
+
},
|
| 332 |
+
"EC": {
|
| 333 |
+
"name": "Ecuador",
|
| 334 |
+
"lat": -2,
|
| 335 |
+
"lng": -77.5
|
| 336 |
+
},
|
| 337 |
+
"EG": {
|
| 338 |
+
"name": "Egypt",
|
| 339 |
+
"lat": 27,
|
| 340 |
+
"lng": 30
|
| 341 |
+
},
|
| 342 |
+
"ER": {
|
| 343 |
+
"name": "Eritrea",
|
| 344 |
+
"lat": 15,
|
| 345 |
+
"lng": 39
|
| 346 |
+
},
|
| 347 |
+
"EH": {
|
| 348 |
+
"name": "Western Sahara",
|
| 349 |
+
"lat": 24.5,
|
| 350 |
+
"lng": -13
|
| 351 |
+
},
|
| 352 |
+
"ES": {
|
| 353 |
+
"name": "Spain",
|
| 354 |
+
"lat": 40,
|
| 355 |
+
"lng": -4
|
| 356 |
+
},
|
| 357 |
+
"EE": {
|
| 358 |
+
"name": "Estonia",
|
| 359 |
+
"lat": 59,
|
| 360 |
+
"lng": 26
|
| 361 |
+
},
|
| 362 |
+
"ET": {
|
| 363 |
+
"name": "Ethiopia",
|
| 364 |
+
"lat": 8,
|
| 365 |
+
"lng": 38
|
| 366 |
+
},
|
| 367 |
+
"FI": {
|
| 368 |
+
"name": "Finland",
|
| 369 |
+
"lat": 64,
|
| 370 |
+
"lng": 26
|
| 371 |
+
},
|
| 372 |
+
"FJ": {
|
| 373 |
+
"name": "Fiji",
|
| 374 |
+
"lat": -18,
|
| 375 |
+
"lng": 175
|
| 376 |
+
},
|
| 377 |
+
"FK": {
|
| 378 |
+
"name": "Falkland Islands",
|
| 379 |
+
"lat": -51.75,
|
| 380 |
+
"lng": -59
|
| 381 |
+
},
|
| 382 |
+
"FR": {
|
| 383 |
+
"name": "France",
|
| 384 |
+
"lat": 46,
|
| 385 |
+
"lng": 2
|
| 386 |
+
},
|
| 387 |
+
"FO": {
|
| 388 |
+
"name": "Faroe Islands",
|
| 389 |
+
"lat": 62,
|
| 390 |
+
"lng": -7
|
| 391 |
+
},
|
| 392 |
+
"FM": {
|
| 393 |
+
"name": "Micronesia",
|
| 394 |
+
"lat": 6.91666666,
|
| 395 |
+
"lng": 158.25
|
| 396 |
+
},
|
| 397 |
+
"GA": {
|
| 398 |
+
"name": "Gabon",
|
| 399 |
+
"lat": -1,
|
| 400 |
+
"lng": 11.75
|
| 401 |
+
},
|
| 402 |
+
"GB": {
|
| 403 |
+
"name": "United Kingdom",
|
| 404 |
+
"lat": 54,
|
| 405 |
+
"lng": -2
|
| 406 |
+
},
|
| 407 |
+
"GE": {
|
| 408 |
+
"name": "Georgia",
|
| 409 |
+
"lat": 42,
|
| 410 |
+
"lng": 43.5
|
| 411 |
+
},
|
| 412 |
+
"GG": {
|
| 413 |
+
"name": "Guernsey",
|
| 414 |
+
"lat": 49.46666666,
|
| 415 |
+
"lng": -2.58333333
|
| 416 |
+
},
|
| 417 |
+
"GH": {
|
| 418 |
+
"name": "Ghana",
|
| 419 |
+
"lat": 8,
|
| 420 |
+
"lng": -2
|
| 421 |
+
},
|
| 422 |
+
"GI": {
|
| 423 |
+
"name": "Gibraltar",
|
| 424 |
+
"lat": 36.13333333,
|
| 425 |
+
"lng": -5.35
|
| 426 |
+
},
|
| 427 |
+
"GN": {
|
| 428 |
+
"name": "Guinea",
|
| 429 |
+
"lat": 11,
|
| 430 |
+
"lng": -10
|
| 431 |
+
},
|
| 432 |
+
"GP": {
|
| 433 |
+
"name": "Guadeloupe",
|
| 434 |
+
"lat": 16.25,
|
| 435 |
+
"lng": -61.583333
|
| 436 |
+
},
|
| 437 |
+
"GM": {
|
| 438 |
+
"name": "Gambia",
|
| 439 |
+
"lat": 13.46666666,
|
| 440 |
+
"lng": -16.56666666
|
| 441 |
+
},
|
| 442 |
+
"GW": {
|
| 443 |
+
"name": "Guinea-Bissau",
|
| 444 |
+
"lat": 12,
|
| 445 |
+
"lng": -15
|
| 446 |
+
},
|
| 447 |
+
"GQ": {
|
| 448 |
+
"name": "Equatorial Guinea",
|
| 449 |
+
"lat": 2,
|
| 450 |
+
"lng": 10
|
| 451 |
+
},
|
| 452 |
+
"GR": {
|
| 453 |
+
"name": "Greece",
|
| 454 |
+
"lat": 39,
|
| 455 |
+
"lng": 22
|
| 456 |
+
},
|
| 457 |
+
"GD": {
|
| 458 |
+
"name": "Grenada",
|
| 459 |
+
"lat": 12.11666666,
|
| 460 |
+
"lng": -61.66666666
|
| 461 |
+
},
|
| 462 |
+
"GL": {
|
| 463 |
+
"name": "Greenland",
|
| 464 |
+
"lat": 72,
|
| 465 |
+
"lng": -40
|
| 466 |
+
},
|
| 467 |
+
"GT": {
|
| 468 |
+
"name": "Guatemala",
|
| 469 |
+
"lat": 15.5,
|
| 470 |
+
"lng": -90.25
|
| 471 |
+
},
|
| 472 |
+
"GF": {
|
| 473 |
+
"name": "French Guiana",
|
| 474 |
+
"lat": 4,
|
| 475 |
+
"lng": -53
|
| 476 |
+
},
|
| 477 |
+
"GU": {
|
| 478 |
+
"name": "Guam",
|
| 479 |
+
"lat": 13.46666666,
|
| 480 |
+
"lng": 144.78333333
|
| 481 |
+
},
|
| 482 |
+
"GY": {
|
| 483 |
+
"name": "Guyana",
|
| 484 |
+
"lat": 5,
|
| 485 |
+
"lng": -59
|
| 486 |
+
},
|
| 487 |
+
"HK": {
|
| 488 |
+
"name": "Hong Kong",
|
| 489 |
+
"lat": 22.267,
|
| 490 |
+
"lng": 114.188
|
| 491 |
+
},
|
| 492 |
+
"HM": {
|
| 493 |
+
"name": "Heard Island and McDonald Islands",
|
| 494 |
+
"lat": -53.1,
|
| 495 |
+
"lng": 72.51666666
|
| 496 |
+
},
|
| 497 |
+
"HN": {
|
| 498 |
+
"name": "Honduras",
|
| 499 |
+
"lat": 15,
|
| 500 |
+
"lng": -86.5
|
| 501 |
+
},
|
| 502 |
+
"HR": {
|
| 503 |
+
"name": "Croatia",
|
| 504 |
+
"lat": 45.16666666,
|
| 505 |
+
"lng": 15.5
|
| 506 |
+
},
|
| 507 |
+
"HT": {
|
| 508 |
+
"name": "Haiti",
|
| 509 |
+
"lat": 19,
|
| 510 |
+
"lng": -72.41666666
|
| 511 |
+
},
|
| 512 |
+
"HU": {
|
| 513 |
+
"name": "Hungary",
|
| 514 |
+
"lat": 47,
|
| 515 |
+
"lng": 20
|
| 516 |
+
},
|
| 517 |
+
"ID": {
|
| 518 |
+
"name": "Indonesia",
|
| 519 |
+
"lat": -5,
|
| 520 |
+
"lng": 120
|
| 521 |
+
},
|
| 522 |
+
"IM": {
|
| 523 |
+
"name": "Isle of Man",
|
| 524 |
+
"lat": 54.25,
|
| 525 |
+
"lng": -4.5
|
| 526 |
+
},
|
| 527 |
+
"IN": {
|
| 528 |
+
"name": "India",
|
| 529 |
+
"lat": 20,
|
| 530 |
+
"lng": 77
|
| 531 |
+
},
|
| 532 |
+
"IO": {
|
| 533 |
+
"name": "British Indian Ocean Territory",
|
| 534 |
+
"lat": -6,
|
| 535 |
+
"lng": 71.5
|
| 536 |
+
},
|
| 537 |
+
"IE": {
|
| 538 |
+
"name": "Ireland",
|
| 539 |
+
"lat": 53,
|
| 540 |
+
"lng": -8
|
| 541 |
+
},
|
| 542 |
+
"IR": {
|
| 543 |
+
"name": "Iran",
|
| 544 |
+
"lat": 32,
|
| 545 |
+
"lng": 53
|
| 546 |
+
},
|
| 547 |
+
"IQ": {
|
| 548 |
+
"name": "Iraq",
|
| 549 |
+
"lat": 33,
|
| 550 |
+
"lng": 44
|
| 551 |
+
},
|
| 552 |
+
"IS": {
|
| 553 |
+
"name": "Iceland",
|
| 554 |
+
"lat": 65,
|
| 555 |
+
"lng": -18
|
| 556 |
+
},
|
| 557 |
+
"IL": {
|
| 558 |
+
"name": "Israel",
|
| 559 |
+
"lat": 31.47,
|
| 560 |
+
"lng": 35.13
|
| 561 |
+
},
|
| 562 |
+
"IT": {
|
| 563 |
+
"name": "Italy",
|
| 564 |
+
"lat": 42.83333333,
|
| 565 |
+
"lng": 12.83333333
|
| 566 |
+
},
|
| 567 |
+
"JM": {
|
| 568 |
+
"name": "Jamaica",
|
| 569 |
+
"lat": 18.25,
|
| 570 |
+
"lng": -77.5
|
| 571 |
+
},
|
| 572 |
+
"JE": {
|
| 573 |
+
"name": "Jersey",
|
| 574 |
+
"lat": 49.25,
|
| 575 |
+
"lng": -2.16666666
|
| 576 |
+
},
|
| 577 |
+
"JO": {
|
| 578 |
+
"name": "Jordan",
|
| 579 |
+
"lat": 31,
|
| 580 |
+
"lng": 36
|
| 581 |
+
},
|
| 582 |
+
"JP": {
|
| 583 |
+
"name": "Japan",
|
| 584 |
+
"lat": 36,
|
| 585 |
+
"lng": 138
|
| 586 |
+
},
|
| 587 |
+
"KZ": {
|
| 588 |
+
"name": "Kazakhstan",
|
| 589 |
+
"lat": 48,
|
| 590 |
+
"lng": 68
|
| 591 |
+
},
|
| 592 |
+
"KE": {
|
| 593 |
+
"name": "Kenya",
|
| 594 |
+
"lat": 1,
|
| 595 |
+
"lng": 38
|
| 596 |
+
},
|
| 597 |
+
"KG": {
|
| 598 |
+
"name": "Kyrgyzstan",
|
| 599 |
+
"lat": 41,
|
| 600 |
+
"lng": 75
|
| 601 |
+
},
|
| 602 |
+
"KH": {
|
| 603 |
+
"name": "Cambodia",
|
| 604 |
+
"lat": 13,
|
| 605 |
+
"lng": 105
|
| 606 |
+
},
|
| 607 |
+
"KI": {
|
| 608 |
+
"name": "Kiribati",
|
| 609 |
+
"lat": 1.41666666,
|
| 610 |
+
"lng": 173
|
| 611 |
+
},
|
| 612 |
+
"KN": {
|
| 613 |
+
"name": "Saint Kitts and Nevis",
|
| 614 |
+
"lat": 17.33333333,
|
| 615 |
+
"lng": -62.75
|
| 616 |
+
},
|
| 617 |
+
"KR": {
|
| 618 |
+
"name": "South Korea",
|
| 619 |
+
"lat": 37,
|
| 620 |
+
"lng": 127.5
|
| 621 |
+
},
|
| 622 |
+
"XK": {
|
| 623 |
+
"name": "Kosovo",
|
| 624 |
+
"lat": 42.666667,
|
| 625 |
+
"lng": 21.166667
|
| 626 |
+
},
|
| 627 |
+
"KW": {
|
| 628 |
+
"name": "Kuwait",
|
| 629 |
+
"lat": 29.5,
|
| 630 |
+
"lng": 45.75
|
| 631 |
+
},
|
| 632 |
+
"LA": {
|
| 633 |
+
"name": "Laos",
|
| 634 |
+
"lat": 18,
|
| 635 |
+
"lng": 105
|
| 636 |
+
},
|
| 637 |
+
"LB": {
|
| 638 |
+
"name": "Lebanon",
|
| 639 |
+
"lat": 33.83333333,
|
| 640 |
+
"lng": 35.83333333
|
| 641 |
+
},
|
| 642 |
+
"LR": {
|
| 643 |
+
"name": "Liberia",
|
| 644 |
+
"lat": 6.5,
|
| 645 |
+
"lng": -9.5
|
| 646 |
+
},
|
| 647 |
+
"LY": {
|
| 648 |
+
"name": "Libya",
|
| 649 |
+
"lat": 25,
|
| 650 |
+
"lng": 17
|
| 651 |
+
},
|
| 652 |
+
"LC": {
|
| 653 |
+
"name": "Saint Lucia",
|
| 654 |
+
"lat": 13.88333333,
|
| 655 |
+
"lng": -60.96666666
|
| 656 |
+
},
|
| 657 |
+
"LI": {
|
| 658 |
+
"name": "Liechtenstein",
|
| 659 |
+
"lat": 47.26666666,
|
| 660 |
+
"lng": 9.53333333
|
| 661 |
+
},
|
| 662 |
+
"LK": {
|
| 663 |
+
"name": "Sri Lanka",
|
| 664 |
+
"lat": 7,
|
| 665 |
+
"lng": 81
|
| 666 |
+
},
|
| 667 |
+
"LS": {
|
| 668 |
+
"name": "Lesotho",
|
| 669 |
+
"lat": -29.5,
|
| 670 |
+
"lng": 28.5
|
| 671 |
+
},
|
| 672 |
+
"LT": {
|
| 673 |
+
"name": "Lithuania",
|
| 674 |
+
"lat": 56,
|
| 675 |
+
"lng": 24
|
| 676 |
+
},
|
| 677 |
+
"LU": {
|
| 678 |
+
"name": "Luxembourg",
|
| 679 |
+
"lat": 49.75,
|
| 680 |
+
"lng": 6.16666666
|
| 681 |
+
},
|
| 682 |
+
"LV": {
|
| 683 |
+
"name": "Latvia",
|
| 684 |
+
"lat": 57,
|
| 685 |
+
"lng": 25
|
| 686 |
+
},
|
| 687 |
+
"MO": {
|
| 688 |
+
"name": "Macau",
|
| 689 |
+
"lat": 22.16666666,
|
| 690 |
+
"lng": 113.55
|
| 691 |
+
},
|
| 692 |
+
"MF": {
|
| 693 |
+
"name": "Saint Martin",
|
| 694 |
+
"lat": 18.08333333,
|
| 695 |
+
"lng": -63.95
|
| 696 |
+
},
|
| 697 |
+
"MA": {
|
| 698 |
+
"name": "Morocco",
|
| 699 |
+
"lat": 32,
|
| 700 |
+
"lng": -5
|
| 701 |
+
},
|
| 702 |
+
"MC": {
|
| 703 |
+
"name": "Monaco",
|
| 704 |
+
"lat": 43.73333333,
|
| 705 |
+
"lng": 7.4
|
| 706 |
+
},
|
| 707 |
+
"MD": {
|
| 708 |
+
"name": "Moldova",
|
| 709 |
+
"lat": 47,
|
| 710 |
+
"lng": 29
|
| 711 |
+
},
|
| 712 |
+
"MG": {
|
| 713 |
+
"name": "Madagascar",
|
| 714 |
+
"lat": -20,
|
| 715 |
+
"lng": 47
|
| 716 |
+
},
|
| 717 |
+
"MV": {
|
| 718 |
+
"name": "Maldives",
|
| 719 |
+
"lat": 3.25,
|
| 720 |
+
"lng": 73
|
| 721 |
+
},
|
| 722 |
+
"MX": {
|
| 723 |
+
"name": "Mexico",
|
| 724 |
+
"lat": 23,
|
| 725 |
+
"lng": -102
|
| 726 |
+
},
|
| 727 |
+
"MH": {
|
| 728 |
+
"name": "Marshall Islands",
|
| 729 |
+
"lat": 9,
|
| 730 |
+
"lng": 168
|
| 731 |
+
},
|
| 732 |
+
"MK": {
|
| 733 |
+
"name": "North Macedonia",
|
| 734 |
+
"lat": 41.83333333,
|
| 735 |
+
"lng": 22
|
| 736 |
+
},
|
| 737 |
+
"ML": {
|
| 738 |
+
"name": "Mali",
|
| 739 |
+
"lat": 17,
|
| 740 |
+
"lng": -4
|
| 741 |
+
},
|
| 742 |
+
"MT": {
|
| 743 |
+
"name": "Malta",
|
| 744 |
+
"lat": 35.83333333,
|
| 745 |
+
"lng": 14.58333333
|
| 746 |
+
},
|
| 747 |
+
"MM": {
|
| 748 |
+
"name": "Myanmar",
|
| 749 |
+
"lat": 22,
|
| 750 |
+
"lng": 98
|
| 751 |
+
},
|
| 752 |
+
"ME": {
|
| 753 |
+
"name": "Montenegro",
|
| 754 |
+
"lat": 42.5,
|
| 755 |
+
"lng": 19.3
|
| 756 |
+
},
|
| 757 |
+
"MN": {
|
| 758 |
+
"name": "Mongolia",
|
| 759 |
+
"lat": 46,
|
| 760 |
+
"lng": 105
|
| 761 |
+
},
|
| 762 |
+
"MP": {
|
| 763 |
+
"name": "Northern Mariana Islands",
|
| 764 |
+
"lat": 15.2,
|
| 765 |
+
"lng": 145.75
|
| 766 |
+
},
|
| 767 |
+
"MZ": {
|
| 768 |
+
"name": "Mozambique",
|
| 769 |
+
"lat": -18.25,
|
| 770 |
+
"lng": 35
|
| 771 |
+
},
|
| 772 |
+
"MR": {
|
| 773 |
+
"name": "Mauritania",
|
| 774 |
+
"lat": 20,
|
| 775 |
+
"lng": -12
|
| 776 |
+
},
|
| 777 |
+
"MS": {
|
| 778 |
+
"name": "Montserrat",
|
| 779 |
+
"lat": 16.75,
|
| 780 |
+
"lng": -62.2
|
| 781 |
+
},
|
| 782 |
+
"MQ": {
|
| 783 |
+
"name": "Martinique",
|
| 784 |
+
"lat": 14.666667,
|
| 785 |
+
"lng": -61
|
| 786 |
+
},
|
| 787 |
+
"MU": {
|
| 788 |
+
"name": "Mauritius",
|
| 789 |
+
"lat": -20.28333333,
|
| 790 |
+
"lng": 57.55
|
| 791 |
+
},
|
| 792 |
+
"MW": {
|
| 793 |
+
"name": "Malawi",
|
| 794 |
+
"lat": -13.5,
|
| 795 |
+
"lng": 34
|
| 796 |
+
},
|
| 797 |
+
"MY": {
|
| 798 |
+
"name": "Malaysia",
|
| 799 |
+
"lat": 2.5,
|
| 800 |
+
"lng": 112.5
|
| 801 |
+
},
|
| 802 |
+
"YT": {
|
| 803 |
+
"name": "Mayotte",
|
| 804 |
+
"lat": -12.83333333,
|
| 805 |
+
"lng": 45.16666666
|
| 806 |
+
},
|
| 807 |
+
"NA": {
|
| 808 |
+
"name": "Namibia",
|
| 809 |
+
"lat": -22,
|
| 810 |
+
"lng": 17
|
| 811 |
+
},
|
| 812 |
+
"NC": {
|
| 813 |
+
"name": "New Caledonia",
|
| 814 |
+
"lat": -21.5,
|
| 815 |
+
"lng": 165.5
|
| 816 |
+
},
|
| 817 |
+
"NE": {
|
| 818 |
+
"name": "Niger",
|
| 819 |
+
"lat": 16,
|
| 820 |
+
"lng": 8
|
| 821 |
+
},
|
| 822 |
+
"NF": {
|
| 823 |
+
"name": "Norfolk Island",
|
| 824 |
+
"lat": -29.03333333,
|
| 825 |
+
"lng": 167.95
|
| 826 |
+
},
|
| 827 |
+
"NG": {
|
| 828 |
+
"name": "Nigeria",
|
| 829 |
+
"lat": 10,
|
| 830 |
+
"lng": 8
|
| 831 |
+
},
|
| 832 |
+
"NI": {
|
| 833 |
+
"name": "Nicaragua",
|
| 834 |
+
"lat": 13,
|
| 835 |
+
"lng": -85
|
| 836 |
+
},
|
| 837 |
+
"NU": {
|
| 838 |
+
"name": "Niue",
|
| 839 |
+
"lat": -19.03333333,
|
| 840 |
+
"lng": -169.86666666
|
| 841 |
+
},
|
| 842 |
+
"NL": {
|
| 843 |
+
"name": "Netherlands",
|
| 844 |
+
"lat": 52.5,
|
| 845 |
+
"lng": 5.75
|
| 846 |
+
},
|
| 847 |
+
"NO": {
|
| 848 |
+
"name": "Norway",
|
| 849 |
+
"lat": 62,
|
| 850 |
+
"lng": 10
|
| 851 |
+
},
|
| 852 |
+
"NP": {
|
| 853 |
+
"name": "Nepal",
|
| 854 |
+
"lat": 28,
|
| 855 |
+
"lng": 84
|
| 856 |
+
},
|
| 857 |
+
"NR": {
|
| 858 |
+
"name": "Nauru",
|
| 859 |
+
"lat": -0.53333333,
|
| 860 |
+
"lng": 166.91666666
|
| 861 |
+
},
|
| 862 |
+
"NZ": {
|
| 863 |
+
"name": "New Zealand",
|
| 864 |
+
"lat": -41,
|
| 865 |
+
"lng": 174
|
| 866 |
+
},
|
| 867 |
+
"OM": {
|
| 868 |
+
"name": "Oman",
|
| 869 |
+
"lat": 21,
|
| 870 |
+
"lng": 57
|
| 871 |
+
},
|
| 872 |
+
"PK": {
|
| 873 |
+
"name": "Pakistan",
|
| 874 |
+
"lat": 30,
|
| 875 |
+
"lng": 70
|
| 876 |
+
},
|
| 877 |
+
"PA": {
|
| 878 |
+
"name": "Panama",
|
| 879 |
+
"lat": 9,
|
| 880 |
+
"lng": -80
|
| 881 |
+
},
|
| 882 |
+
"PN": {
|
| 883 |
+
"name": "Pitcairn Islands",
|
| 884 |
+
"lat": -25.06666666,
|
| 885 |
+
"lng": -130.1
|
| 886 |
+
},
|
| 887 |
+
"PE": {
|
| 888 |
+
"name": "Peru",
|
| 889 |
+
"lat": -10,
|
| 890 |
+
"lng": -76
|
| 891 |
+
},
|
| 892 |
+
"PH": {
|
| 893 |
+
"name": "Philippines",
|
| 894 |
+
"lat": 13,
|
| 895 |
+
"lng": 122
|
| 896 |
+
},
|
| 897 |
+
"PW": {
|
| 898 |
+
"name": "Palau",
|
| 899 |
+
"lat": 7.5,
|
| 900 |
+
"lng": 134.5
|
| 901 |
+
},
|
| 902 |
+
"PG": {
|
| 903 |
+
"name": "Papua New Guinea",
|
| 904 |
+
"lat": -6,
|
| 905 |
+
"lng": 147
|
| 906 |
+
},
|
| 907 |
+
"PL": {
|
| 908 |
+
"name": "Poland",
|
| 909 |
+
"lat": 52,
|
| 910 |
+
"lng": 20
|
| 911 |
+
},
|
| 912 |
+
"PR": {
|
| 913 |
+
"name": "Puerto Rico",
|
| 914 |
+
"lat": 18.25,
|
| 915 |
+
"lng": -66.5
|
| 916 |
+
},
|
| 917 |
+
"KP": {
|
| 918 |
+
"name": "North Korea",
|
| 919 |
+
"lat": 40,
|
| 920 |
+
"lng": 127
|
| 921 |
+
},
|
| 922 |
+
"PT": {
|
| 923 |
+
"name": "Portugal",
|
| 924 |
+
"lat": 39.5,
|
| 925 |
+
"lng": -8
|
| 926 |
+
},
|
| 927 |
+
"PY": {
|
| 928 |
+
"name": "Paraguay",
|
| 929 |
+
"lat": -23,
|
| 930 |
+
"lng": -58
|
| 931 |
+
},
|
| 932 |
+
"PS": {
|
| 933 |
+
"name": "Palestine",
|
| 934 |
+
"lat": 31.9,
|
| 935 |
+
"lng": 35.2
|
| 936 |
+
},
|
| 937 |
+
"PF": {
|
| 938 |
+
"name": "French Polynesia",
|
| 939 |
+
"lat": -15,
|
| 940 |
+
"lng": -140
|
| 941 |
+
},
|
| 942 |
+
"QA": {
|
| 943 |
+
"name": "Qatar",
|
| 944 |
+
"lat": 25.5,
|
| 945 |
+
"lng": 51.25
|
| 946 |
+
},
|
| 947 |
+
"RE": {
|
| 948 |
+
"name": "Réunion",
|
| 949 |
+
"lat": -21.15,
|
| 950 |
+
"lng": 55.5
|
| 951 |
+
},
|
| 952 |
+
"RO": {
|
| 953 |
+
"name": "Romania",
|
| 954 |
+
"lat": 46,
|
| 955 |
+
"lng": 25
|
| 956 |
+
},
|
| 957 |
+
"RU": {
|
| 958 |
+
"name": "Russia",
|
| 959 |
+
"lat": 60,
|
| 960 |
+
"lng": 100
|
| 961 |
+
},
|
| 962 |
+
"RW": {
|
| 963 |
+
"name": "Rwanda",
|
| 964 |
+
"lat": -2,
|
| 965 |
+
"lng": 30
|
| 966 |
+
},
|
| 967 |
+
"SA": {
|
| 968 |
+
"name": "Saudi Arabia",
|
| 969 |
+
"lat": 25,
|
| 970 |
+
"lng": 45
|
| 971 |
+
},
|
| 972 |
+
"SD": {
|
| 973 |
+
"name": "Sudan",
|
| 974 |
+
"lat": 15,
|
| 975 |
+
"lng": 30
|
| 976 |
+
},
|
| 977 |
+
"SN": {
|
| 978 |
+
"name": "Senegal",
|
| 979 |
+
"lat": 14,
|
| 980 |
+
"lng": -14
|
| 981 |
+
},
|
| 982 |
+
"SG": {
|
| 983 |
+
"name": "Singapore",
|
| 984 |
+
"lat": 1.36666666,
|
| 985 |
+
"lng": 103.8
|
| 986 |
+
},
|
| 987 |
+
"GS": {
|
| 988 |
+
"name": "South Georgia",
|
| 989 |
+
"lat": -54.5,
|
| 990 |
+
"lng": -37
|
| 991 |
+
},
|
| 992 |
+
"SJ": {
|
| 993 |
+
"name": "Svalbard and Jan Mayen",
|
| 994 |
+
"lat": 78,
|
| 995 |
+
"lng": 20
|
| 996 |
+
},
|
| 997 |
+
"SB": {
|
| 998 |
+
"name": "Solomon Islands",
|
| 999 |
+
"lat": -8,
|
| 1000 |
+
"lng": 159
|
| 1001 |
+
},
|
| 1002 |
+
"SL": {
|
| 1003 |
+
"name": "Sierra Leone",
|
| 1004 |
+
"lat": 8.5,
|
| 1005 |
+
"lng": -11.5
|
| 1006 |
+
},
|
| 1007 |
+
"SV": {
|
| 1008 |
+
"name": "El Salvador",
|
| 1009 |
+
"lat": 13.83333333,
|
| 1010 |
+
"lng": -88.91666666
|
| 1011 |
+
},
|
| 1012 |
+
"SM": {
|
| 1013 |
+
"name": "San Marino",
|
| 1014 |
+
"lat": 43.76666666,
|
| 1015 |
+
"lng": 12.41666666
|
| 1016 |
+
},
|
| 1017 |
+
"SO": {
|
| 1018 |
+
"name": "Somalia",
|
| 1019 |
+
"lat": 10,
|
| 1020 |
+
"lng": 49
|
| 1021 |
+
},
|
| 1022 |
+
"PM": {
|
| 1023 |
+
"name": "Saint Pierre and Miquelon",
|
| 1024 |
+
"lat": 46.83333333,
|
| 1025 |
+
"lng": -56.33333333
|
| 1026 |
+
},
|
| 1027 |
+
"RS": {
|
| 1028 |
+
"name": "Serbia",
|
| 1029 |
+
"lat": 44,
|
| 1030 |
+
"lng": 21
|
| 1031 |
+
},
|
| 1032 |
+
"SS": {
|
| 1033 |
+
"name": "South Sudan",
|
| 1034 |
+
"lat": 7,
|
| 1035 |
+
"lng": 30
|
| 1036 |
+
},
|
| 1037 |
+
"ST": {
|
| 1038 |
+
"name": "São Tomé and Príncipe",
|
| 1039 |
+
"lat": 1,
|
| 1040 |
+
"lng": 7
|
| 1041 |
+
},
|
| 1042 |
+
"SR": {
|
| 1043 |
+
"name": "Suriname",
|
| 1044 |
+
"lat": 4,
|
| 1045 |
+
"lng": -56
|
| 1046 |
+
},
|
| 1047 |
+
"SK": {
|
| 1048 |
+
"name": "Slovakia",
|
| 1049 |
+
"lat": 48.66666666,
|
| 1050 |
+
"lng": 19.5
|
| 1051 |
+
},
|
| 1052 |
+
"SI": {
|
| 1053 |
+
"name": "Slovenia",
|
| 1054 |
+
"lat": 46.11666666,
|
| 1055 |
+
"lng": 14.81666666
|
| 1056 |
+
},
|
| 1057 |
+
"SE": {
|
| 1058 |
+
"name": "Sweden",
|
| 1059 |
+
"lat": 62,
|
| 1060 |
+
"lng": 15
|
| 1061 |
+
},
|
| 1062 |
+
"SZ": {
|
| 1063 |
+
"name": "Eswatini",
|
| 1064 |
+
"lat": -26.5,
|
| 1065 |
+
"lng": 31.5
|
| 1066 |
+
},
|
| 1067 |
+
"SX": {
|
| 1068 |
+
"name": "Sint Maarten",
|
| 1069 |
+
"lat": 18.033333,
|
| 1070 |
+
"lng": -63.05
|
| 1071 |
+
},
|
| 1072 |
+
"SC": {
|
| 1073 |
+
"name": "Seychelles",
|
| 1074 |
+
"lat": -4.58333333,
|
| 1075 |
+
"lng": 55.66666666
|
| 1076 |
+
},
|
| 1077 |
+
"SY": {
|
| 1078 |
+
"name": "Syria",
|
| 1079 |
+
"lat": 35,
|
| 1080 |
+
"lng": 38
|
| 1081 |
+
},
|
| 1082 |
+
"TC": {
|
| 1083 |
+
"name": "Turks and Caicos Islands",
|
| 1084 |
+
"lat": 21.75,
|
| 1085 |
+
"lng": -71.58333333
|
| 1086 |
+
},
|
| 1087 |
+
"TD": {
|
| 1088 |
+
"name": "Chad",
|
| 1089 |
+
"lat": 15,
|
| 1090 |
+
"lng": 19
|
| 1091 |
+
},
|
| 1092 |
+
"TG": {
|
| 1093 |
+
"name": "Togo",
|
| 1094 |
+
"lat": 8,
|
| 1095 |
+
"lng": 1.16666666
|
| 1096 |
+
},
|
| 1097 |
+
"TH": {
|
| 1098 |
+
"name": "Thailand",
|
| 1099 |
+
"lat": 15,
|
| 1100 |
+
"lng": 100
|
| 1101 |
+
},
|
| 1102 |
+
"TJ": {
|
| 1103 |
+
"name": "Tajikistan",
|
| 1104 |
+
"lat": 39,
|
| 1105 |
+
"lng": 71
|
| 1106 |
+
},
|
| 1107 |
+
"TK": {
|
| 1108 |
+
"name": "Tokelau",
|
| 1109 |
+
"lat": -9,
|
| 1110 |
+
"lng": -172
|
| 1111 |
+
},
|
| 1112 |
+
"TM": {
|
| 1113 |
+
"name": "Turkmenistan",
|
| 1114 |
+
"lat": 40,
|
| 1115 |
+
"lng": 60
|
| 1116 |
+
},
|
| 1117 |
+
"TL": {
|
| 1118 |
+
"name": "Timor-Leste",
|
| 1119 |
+
"lat": -8.83333333,
|
| 1120 |
+
"lng": 125.91666666
|
| 1121 |
+
},
|
| 1122 |
+
"TO": {
|
| 1123 |
+
"name": "Tonga",
|
| 1124 |
+
"lat": -20,
|
| 1125 |
+
"lng": -175
|
| 1126 |
+
},
|
| 1127 |
+
"TT": {
|
| 1128 |
+
"name": "Trinidad and Tobago",
|
| 1129 |
+
"lat": 11,
|
| 1130 |
+
"lng": -61
|
| 1131 |
+
},
|
| 1132 |
+
"TN": {
|
| 1133 |
+
"name": "Tunisia",
|
| 1134 |
+
"lat": 34,
|
| 1135 |
+
"lng": 9
|
| 1136 |
+
},
|
| 1137 |
+
"TR": {
|
| 1138 |
+
"name": "Türkiye",
|
| 1139 |
+
"lat": 39,
|
| 1140 |
+
"lng": 35
|
| 1141 |
+
},
|
| 1142 |
+
"TV": {
|
| 1143 |
+
"name": "Tuvalu",
|
| 1144 |
+
"lat": -8,
|
| 1145 |
+
"lng": 178
|
| 1146 |
+
},
|
| 1147 |
+
"TW": {
|
| 1148 |
+
"name": "Taiwan",
|
| 1149 |
+
"lat": 23.5,
|
| 1150 |
+
"lng": 121
|
| 1151 |
+
},
|
| 1152 |
+
"TZ": {
|
| 1153 |
+
"name": "Tanzania",
|
| 1154 |
+
"lat": -6,
|
| 1155 |
+
"lng": 35
|
| 1156 |
+
},
|
| 1157 |
+
"UG": {
|
| 1158 |
+
"name": "Uganda",
|
| 1159 |
+
"lat": 1,
|
| 1160 |
+
"lng": 32
|
| 1161 |
+
},
|
| 1162 |
+
"UA": {
|
| 1163 |
+
"name": "Ukraine",
|
| 1164 |
+
"lat": 49,
|
| 1165 |
+
"lng": 32
|
| 1166 |
+
},
|
| 1167 |
+
"UM": {
|
| 1168 |
+
"name": "United States Minor Outlying Islands",
|
| 1169 |
+
"lat": 19.3,
|
| 1170 |
+
"lng": 166.633333
|
| 1171 |
+
},
|
| 1172 |
+
"UY": {
|
| 1173 |
+
"name": "Uruguay",
|
| 1174 |
+
"lat": -33,
|
| 1175 |
+
"lng": -56
|
| 1176 |
+
},
|
| 1177 |
+
"US": {
|
| 1178 |
+
"name": "United States",
|
| 1179 |
+
"lat": 38,
|
| 1180 |
+
"lng": -97
|
| 1181 |
+
},
|
| 1182 |
+
"UZ": {
|
| 1183 |
+
"name": "Uzbekistan",
|
| 1184 |
+
"lat": 41,
|
| 1185 |
+
"lng": 64
|
| 1186 |
+
},
|
| 1187 |
+
"VA": {
|
| 1188 |
+
"name": "Vatican City",
|
| 1189 |
+
"lat": 41.9,
|
| 1190 |
+
"lng": 12.45
|
| 1191 |
+
},
|
| 1192 |
+
"VC": {
|
| 1193 |
+
"name": "Saint Vincent and the Grenadines",
|
| 1194 |
+
"lat": 13.25,
|
| 1195 |
+
"lng": -61.2
|
| 1196 |
+
},
|
| 1197 |
+
"VE": {
|
| 1198 |
+
"name": "Venezuela",
|
| 1199 |
+
"lat": 8,
|
| 1200 |
+
"lng": -66
|
| 1201 |
+
},
|
| 1202 |
+
"VG": {
|
| 1203 |
+
"name": "British Virgin Islands",
|
| 1204 |
+
"lat": 18.431383,
|
| 1205 |
+
"lng": -64.62305
|
| 1206 |
+
},
|
| 1207 |
+
"VI": {
|
| 1208 |
+
"name": "United States Virgin Islands",
|
| 1209 |
+
"lat": 18.35,
|
| 1210 |
+
"lng": -64.933333
|
| 1211 |
+
},
|
| 1212 |
+
"VN": {
|
| 1213 |
+
"name": "Vietnam",
|
| 1214 |
+
"lat": 16.16666666,
|
| 1215 |
+
"lng": 107.83333333
|
| 1216 |
+
},
|
| 1217 |
+
"VU": {
|
| 1218 |
+
"name": "Vanuatu",
|
| 1219 |
+
"lat": -16,
|
| 1220 |
+
"lng": 167
|
| 1221 |
+
},
|
| 1222 |
+
"WF": {
|
| 1223 |
+
"name": "Wallis and Futuna",
|
| 1224 |
+
"lat": -13.3,
|
| 1225 |
+
"lng": -176.2
|
| 1226 |
+
},
|
| 1227 |
+
"WS": {
|
| 1228 |
+
"name": "Samoa",
|
| 1229 |
+
"lat": -13.58333333,
|
| 1230 |
+
"lng": -172.33333333
|
| 1231 |
+
},
|
| 1232 |
+
"YE": {
|
| 1233 |
+
"name": "Yemen",
|
| 1234 |
+
"lat": 15,
|
| 1235 |
+
"lng": 48
|
| 1236 |
+
},
|
| 1237 |
+
"ZA": {
|
| 1238 |
+
"name": "South Africa",
|
| 1239 |
+
"lat": -29,
|
| 1240 |
+
"lng": 24
|
| 1241 |
+
},
|
| 1242 |
+
"ZM": {
|
| 1243 |
+
"name": "Zambia",
|
| 1244 |
+
"lat": -15,
|
| 1245 |
+
"lng": 30
|
| 1246 |
+
},
|
| 1247 |
+
"ZW": {
|
| 1248 |
+
"name": "Zimbabwe",
|
| 1249 |
+
"lat": -20,
|
| 1250 |
+
"lng": 30
|
| 1251 |
+
}
|
| 1252 |
+
}
|
assets/globe.css
CHANGED
|
@@ -14,4 +14,19 @@
|
|
| 14 |
height: 100%;
|
| 15 |
min-height: calc(100vh - 6rem);
|
| 16 |
background: #0d1117;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
}
|
|
|
|
| 14 |
height: 100%;
|
| 15 |
min-height: calc(100vh - 6rem);
|
| 16 |
background: #0d1117;
|
| 17 |
+
border-radius: 12px;
|
| 18 |
+
overflow: hidden;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
.maplibregl-popup-content {
|
| 22 |
+
background: #111827;
|
| 23 |
+
color: #f9fafb;
|
| 24 |
+
border-radius: 8px;
|
| 25 |
+
padding: 8px 10px;
|
| 26 |
+
font-size: 13px;
|
| 27 |
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
.maplibregl-popup-tip {
|
| 31 |
+
border-top-color: #111827;
|
| 32 |
}
|
assets/globe.js
CHANGED
|
@@ -47,7 +47,7 @@ const GLOBE_DEFAULTS = {
|
|
| 47 |
touchZoomRotate: true,
|
| 48 |
interactive: true,
|
| 49 |
showGeolocateControl: false,
|
| 50 |
-
useCurrentLocation:
|
| 51 |
geolocateZoom: 10,
|
| 52 |
showUserLocation: true,
|
| 53 |
};
|
|
@@ -114,6 +114,136 @@ function mergeGlobeOptions(...sources) {
|
|
| 114 |
return merged;
|
| 115 |
}
|
| 116 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
async function initGlobe(options = {}) {
|
| 118 |
try {
|
| 119 |
await ensureMapLibre();
|
|
@@ -157,6 +287,9 @@ async function initGlobe(options = {}) {
|
|
| 157 |
...mapOptions,
|
| 158 |
});
|
| 159 |
|
|
|
|
|
|
|
|
|
|
| 160 |
let userMarker = null;
|
| 161 |
|
| 162 |
if (showGeolocateControl) {
|
|
@@ -203,9 +336,10 @@ async function initGlobe(options = {}) {
|
|
| 203 |
|
| 204 |
map.on("load", () => {
|
| 205 |
map.resize();
|
| 206 |
-
if (useCurrentLocation) {
|
| 207 |
flyToCurrentLocation();
|
| 208 |
}
|
|
|
|
| 209 |
});
|
| 210 |
|
| 211 |
window.addEventListener("resize", () => map.resize());
|
|
|
|
| 47 |
touchZoomRotate: true,
|
| 48 |
interactive: true,
|
| 49 |
showGeolocateControl: false,
|
| 50 |
+
useCurrentLocation: false,
|
| 51 |
geolocateZoom: 10,
|
| 52 |
showUserLocation: true,
|
| 53 |
};
|
|
|
|
| 114 |
return merged;
|
| 115 |
}
|
| 116 |
|
| 117 |
+
const HIGHLIGHT_SOURCE = "borderless-highlights";
|
| 118 |
+
const HIGHLIGHT_LAYER = "borderless-highlight-circles";
|
| 119 |
+
|
| 120 |
+
window.BorderlessGlobe = {
|
| 121 |
+
map: null,
|
| 122 |
+
markers: [],
|
| 123 |
+
lastVersion: -1,
|
| 124 |
+
pendingState: null,
|
| 125 |
+
rootElement: null,
|
| 126 |
+
|
| 127 |
+
clearMarkers() {
|
| 128 |
+
for (const marker of this.markers) {
|
| 129 |
+
marker.remove();
|
| 130 |
+
}
|
| 131 |
+
this.markers = [];
|
| 132 |
+
},
|
| 133 |
+
|
| 134 |
+
clearHighlights() {
|
| 135 |
+
if (!this.map) return;
|
| 136 |
+
if (this.map.getLayer(HIGHLIGHT_LAYER)) {
|
| 137 |
+
this.map.removeLayer(HIGHLIGHT_LAYER);
|
| 138 |
+
}
|
| 139 |
+
if (this.map.getSource(HIGHLIGHT_SOURCE)) {
|
| 140 |
+
this.map.removeSource(HIGHLIGHT_SOURCE);
|
| 141 |
+
}
|
| 142 |
+
},
|
| 143 |
+
|
| 144 |
+
addMarkers(markers) {
|
| 145 |
+
if (!this.map || !markers?.length) return;
|
| 146 |
+
for (const marker of markers) {
|
| 147 |
+
const popup = marker.label
|
| 148 |
+
? new maplibregl.Popup({ offset: 18, closeButton: false }).setText(marker.label)
|
| 149 |
+
: null;
|
| 150 |
+
const instance = new maplibregl.Marker({ color: "#f59e0b" })
|
| 151 |
+
.setLngLat([marker.lng, marker.lat])
|
| 152 |
+
.addTo(this.map);
|
| 153 |
+
if (popup) {
|
| 154 |
+
instance.setPopup(popup);
|
| 155 |
+
}
|
| 156 |
+
this.markers.push(instance);
|
| 157 |
+
}
|
| 158 |
+
},
|
| 159 |
+
|
| 160 |
+
addHighlights(highlights) {
|
| 161 |
+
if (!this.map || !highlights?.length) return;
|
| 162 |
+
const features = highlights.map((item) => ({
|
| 163 |
+
type: "Feature",
|
| 164 |
+
geometry: {
|
| 165 |
+
type: "Point",
|
| 166 |
+
coordinates: [item.lng, item.lat],
|
| 167 |
+
},
|
| 168 |
+
properties: {
|
| 169 |
+
iso2: item.iso2,
|
| 170 |
+
},
|
| 171 |
+
}));
|
| 172 |
+
|
| 173 |
+
this.map.addSource(HIGHLIGHT_SOURCE, {
|
| 174 |
+
type: "geojson",
|
| 175 |
+
data: {
|
| 176 |
+
type: "FeatureCollection",
|
| 177 |
+
features,
|
| 178 |
+
},
|
| 179 |
+
});
|
| 180 |
+
|
| 181 |
+
this.map.addLayer({
|
| 182 |
+
id: HIGHLIGHT_LAYER,
|
| 183 |
+
type: "circle",
|
| 184 |
+
source: HIGHLIGHT_SOURCE,
|
| 185 |
+
paint: {
|
| 186 |
+
"circle-radius": [
|
| 187 |
+
"interpolate",
|
| 188 |
+
["linear"],
|
| 189 |
+
["zoom"],
|
| 190 |
+
1,
|
| 191 |
+
18,
|
| 192 |
+
4,
|
| 193 |
+
28,
|
| 194 |
+
8,
|
| 195 |
+
42,
|
| 196 |
+
],
|
| 197 |
+
"circle-color": "#f59e0b",
|
| 198 |
+
"circle-opacity": 0.28,
|
| 199 |
+
"circle-stroke-width": 2,
|
| 200 |
+
"circle-stroke-color": "#fbbf24",
|
| 201 |
+
"circle-stroke-opacity": 0.9,
|
| 202 |
+
},
|
| 203 |
+
});
|
| 204 |
+
},
|
| 205 |
+
|
| 206 |
+
applyFlyTo(flyTo) {
|
| 207 |
+
if (!this.map || !flyTo) return;
|
| 208 |
+
if (flyTo.bounds) {
|
| 209 |
+
this.map.fitBounds(flyTo.bounds, {
|
| 210 |
+
padding: flyTo.padding ?? 80,
|
| 211 |
+
duration: 1800,
|
| 212 |
+
essential: true,
|
| 213 |
+
});
|
| 214 |
+
return;
|
| 215 |
+
}
|
| 216 |
+
if (flyTo.center) {
|
| 217 |
+
this.map.flyTo({
|
| 218 |
+
center: flyTo.center,
|
| 219 |
+
zoom: flyTo.zoom ?? 4,
|
| 220 |
+
duration: 1800,
|
| 221 |
+
essential: true,
|
| 222 |
+
});
|
| 223 |
+
}
|
| 224 |
+
},
|
| 225 |
+
|
| 226 |
+
applyState(state) {
|
| 227 |
+
if (!state) return;
|
| 228 |
+
this.pendingState = state;
|
| 229 |
+
if (!this.map) return;
|
| 230 |
+
if (state.version === this.lastVersion) return;
|
| 231 |
+
this.lastVersion = state.version;
|
| 232 |
+
|
| 233 |
+
this.clearMarkers();
|
| 234 |
+
this.clearHighlights();
|
| 235 |
+
this.addMarkers(state.markers || []);
|
| 236 |
+
this.addHighlights(state.highlights || []);
|
| 237 |
+
this.applyFlyTo(state.fly_to);
|
| 238 |
+
},
|
| 239 |
+
|
| 240 |
+
onMapReady() {
|
| 241 |
+
if (this.pendingState) {
|
| 242 |
+
this.applyState(this.pendingState);
|
| 243 |
+
}
|
| 244 |
+
},
|
| 245 |
+
};
|
| 246 |
+
|
| 247 |
async function initGlobe(options = {}) {
|
| 248 |
try {
|
| 249 |
await ensureMapLibre();
|
|
|
|
| 287 |
...mapOptions,
|
| 288 |
});
|
| 289 |
|
| 290 |
+
window.BorderlessGlobe.map = map;
|
| 291 |
+
window.BorderlessGlobe.rootElement = element;
|
| 292 |
+
|
| 293 |
let userMarker = null;
|
| 294 |
|
| 295 |
if (showGeolocateControl) {
|
|
|
|
| 336 |
|
| 337 |
map.on("load", () => {
|
| 338 |
map.resize();
|
| 339 |
+
if (useCurrentLocation && !window.BorderlessGlobe.pendingState?.fly_to) {
|
| 340 |
flyToCurrentLocation();
|
| 341 |
}
|
| 342 |
+
window.BorderlessGlobe.onMapReady();
|
| 343 |
});
|
| 344 |
|
| 345 |
window.addEventListener("resize", () => map.resize());
|
ui/chat.py
CHANGED
|
@@ -2,14 +2,18 @@
|
|
| 2 |
import gradio as gr
|
| 3 |
|
| 4 |
from ui.globe import render_globe_panel
|
|
|
|
| 5 |
from ui.inference import respond
|
| 6 |
|
| 7 |
ADDITIONAL_INPUTS = [
|
| 8 |
gr.Textbox(
|
| 9 |
value=(
|
| 10 |
-
"You are
|
| 11 |
-
"
|
| 12 |
-
"
|
|
|
|
|
|
|
|
|
|
| 13 |
),
|
| 14 |
label="System message",
|
| 15 |
),
|
|
@@ -54,7 +58,9 @@ def create_chat_interface(
|
|
| 54 |
"""
|
| 55 |
For information on how to customize the ChatInterface, peruse the gradio docs: https://www.gradio.app/docs/chatinterface
|
| 56 |
"""
|
| 57 |
-
|
|
|
|
|
|
|
| 58 |
respond,
|
| 59 |
multimodal=True,
|
| 60 |
textbox=gr.MultimodalTextbox(
|
|
@@ -64,5 +70,15 @@ def create_chat_interface(
|
|
| 64 |
sources=["upload"],
|
| 65 |
),
|
| 66 |
history_container=history_container,
|
| 67 |
-
additional_inputs=ADDITIONAL_INPUTS,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
)
|
|
|
|
|
|
| 2 |
import gradio as gr
|
| 3 |
|
| 4 |
from ui.globe import render_globe_panel
|
| 5 |
+
from ui.globe_commands import empty_globe_state
|
| 6 |
from ui.inference import respond
|
| 7 |
|
| 8 |
ADDITIONAL_INPUTS = [
|
| 9 |
gr.Textbox(
|
| 10 |
value=(
|
| 11 |
+
"You are Borderless, an immigration research assistant with access to "
|
| 12 |
+
"World Bank economic data, passport/visa information, and an interactive "
|
| 13 |
+
"3D globe. Use data tools when users ask about GDP, inflation, trade, "
|
| 14 |
+
"visa requirements, or passport strength. When you recommend or compare "
|
| 15 |
+
"destination countries, call update_globe with action show so the user "
|
| 16 |
+
"can see them on the globe."
|
| 17 |
),
|
| 18 |
label="System message",
|
| 19 |
),
|
|
|
|
| 58 |
"""
|
| 59 |
For information on how to customize the ChatInterface, peruse the gradio docs: https://www.gradio.app/docs/chatinterface
|
| 60 |
"""
|
| 61 |
+
globe_state = gr.JSON(value=empty_globe_state(), visible=False, elem_id="globe-state")
|
| 62 |
+
|
| 63 |
+
chat = SidebarChatInterface(
|
| 64 |
respond,
|
| 65 |
multimodal=True,
|
| 66 |
textbox=gr.MultimodalTextbox(
|
|
|
|
| 70 |
sources=["upload"],
|
| 71 |
),
|
| 72 |
history_container=history_container,
|
| 73 |
+
additional_inputs=ADDITIONAL_INPUTS + [globe_state],
|
| 74 |
+
additional_outputs=[globe_state],
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
globe_state.change(
|
| 78 |
+
fn=None,
|
| 79 |
+
inputs=[globe_state],
|
| 80 |
+
outputs=[],
|
| 81 |
+
js="(state) => { window.BorderlessGlobe?.applyState(state); return []; }",
|
| 82 |
+
queue=False,
|
| 83 |
)
|
| 84 |
+
return chat
|
ui/country_coords.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ui/country_coords.py
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import json
|
| 5 |
+
from functools import lru_cache
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
ASSETS_DIR = Path(__file__).resolve().parent.parent / "assets"
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@lru_cache(maxsize=1)
|
| 12 |
+
def _load_centroids() -> dict[str, dict[str, float | str]]:
|
| 13 |
+
return json.loads((ASSETS_DIR / "country_centroids.json").read_text(encoding="utf-8"))
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def lookup_country(iso2: str) -> dict[str, float | str] | None:
|
| 17 |
+
code = (iso2 or "").strip().upper()
|
| 18 |
+
if not code:
|
| 19 |
+
return None
|
| 20 |
+
return _load_centroids().get(code)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def resolve_countries(
|
| 24 |
+
countries: list[str],
|
| 25 |
+
labels: list[str] | None = None,
|
| 26 |
+
) -> tuple[list[dict], list[str]]:
|
| 27 |
+
resolved: list[dict] = []
|
| 28 |
+
unknown: list[str] = []
|
| 29 |
+
label_list = labels or []
|
| 30 |
+
|
| 31 |
+
for index, raw_code in enumerate(countries):
|
| 32 |
+
code = (raw_code or "").strip().upper()
|
| 33 |
+
if not code:
|
| 34 |
+
continue
|
| 35 |
+
info = lookup_country(code)
|
| 36 |
+
if not info:
|
| 37 |
+
unknown.append(code)
|
| 38 |
+
continue
|
| 39 |
+
label = label_list[index] if index < len(label_list) else str(info["name"])
|
| 40 |
+
resolved.append(
|
| 41 |
+
{
|
| 42 |
+
"iso2": code,
|
| 43 |
+
"name": str(info["name"]),
|
| 44 |
+
"label": label,
|
| 45 |
+
"lat": float(info["lat"]),
|
| 46 |
+
"lng": float(info["lng"]),
|
| 47 |
+
}
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
return resolved, unknown
|
ui/globe.py
CHANGED
|
@@ -37,7 +37,7 @@ def _map_data_attrs(
|
|
| 37 |
touch_zoom_rotate: bool = True,
|
| 38 |
interactive: bool = True,
|
| 39 |
show_geolocate_control: bool = False,
|
| 40 |
-
use_current_location: bool =
|
| 41 |
geolocate_zoom: float = 10.0,
|
| 42 |
track_user_location: bool = False,
|
| 43 |
show_user_location: bool = True,
|
|
@@ -94,7 +94,7 @@ def render_globe_panel(
|
|
| 94 |
touch_zoom_rotate: bool = True,
|
| 95 |
interactive: bool = True,
|
| 96 |
show_geolocate_control: bool = False,
|
| 97 |
-
use_current_location: bool =
|
| 98 |
geolocate_zoom: float = 10.0,
|
| 99 |
track_user_location: bool = False,
|
| 100 |
show_user_location: bool = True,
|
|
|
|
| 37 |
touch_zoom_rotate: bool = True,
|
| 38 |
interactive: bool = True,
|
| 39 |
show_geolocate_control: bool = False,
|
| 40 |
+
use_current_location: bool = False,
|
| 41 |
geolocate_zoom: float = 10.0,
|
| 42 |
track_user_location: bool = False,
|
| 43 |
show_user_location: bool = True,
|
|
|
|
| 94 |
touch_zoom_rotate: bool = True,
|
| 95 |
interactive: bool = True,
|
| 96 |
show_geolocate_control: bool = False,
|
| 97 |
+
use_current_location: bool = False,
|
| 98 |
geolocate_zoom: float = 10.0,
|
| 99 |
track_user_location: bool = False,
|
| 100 |
show_user_location: bool = True,
|
ui/globe_commands.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ui/globe_commands.py
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
from typing import Any
|
| 5 |
+
|
| 6 |
+
from ui.country_coords import resolve_countries
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def empty_globe_state() -> dict[str, Any]:
|
| 10 |
+
return {"version": 0, "markers": [], "highlights": [], "fly_to": None}
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def _next_version(state: dict[str, Any]) -> int:
|
| 14 |
+
return int(state.get("version", 0)) + 1
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def _marker_payload(country: dict[str, Any]) -> dict[str, Any]:
|
| 18 |
+
return {
|
| 19 |
+
"iso2": country["iso2"],
|
| 20 |
+
"label": country["label"],
|
| 21 |
+
"lng": country["lng"],
|
| 22 |
+
"lat": country["lat"],
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def _highlight_payload(country: dict[str, Any]) -> dict[str, Any]:
|
| 27 |
+
return {
|
| 28 |
+
"iso2": country["iso2"],
|
| 29 |
+
"lng": country["lng"],
|
| 30 |
+
"lat": country["lat"],
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _fly_to_payload(
|
| 35 |
+
countries: list[dict[str, Any]],
|
| 36 |
+
*,
|
| 37 |
+
zoom: float | None,
|
| 38 |
+
) -> dict[str, Any] | None:
|
| 39 |
+
if not countries:
|
| 40 |
+
return None
|
| 41 |
+
if len(countries) == 1:
|
| 42 |
+
country = countries[0]
|
| 43 |
+
return {
|
| 44 |
+
"center": [country["lng"], country["lat"]],
|
| 45 |
+
"zoom": zoom if zoom is not None else 4.0,
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
lngs = [country["lng"] for country in countries]
|
| 49 |
+
lats = [country["lat"] for country in countries]
|
| 50 |
+
return {
|
| 51 |
+
"bounds": [[min(lngs), min(lats)], [max(lngs), max(lats)]],
|
| 52 |
+
"padding": 80,
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def apply_update_globe(
|
| 57 |
+
state: dict[str, Any],
|
| 58 |
+
args: dict[str, Any],
|
| 59 |
+
) -> tuple[dict[str, Any], dict[str, Any]]:
|
| 60 |
+
action = (args.get("action") or "show").strip().lower()
|
| 61 |
+
countries = args.get("countries") or []
|
| 62 |
+
labels = args.get("labels")
|
| 63 |
+
zoom = args.get("zoom")
|
| 64 |
+
|
| 65 |
+
if action == "clear":
|
| 66 |
+
new_state = empty_globe_state()
|
| 67 |
+
new_state["version"] = _next_version(state)
|
| 68 |
+
return {"status": "cleared", "action": action}, new_state
|
| 69 |
+
|
| 70 |
+
if not countries:
|
| 71 |
+
return (
|
| 72 |
+
{"error": "countries is required unless action is clear"},
|
| 73 |
+
state,
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
resolved, unknown = resolve_countries(countries, labels)
|
| 77 |
+
if not resolved:
|
| 78 |
+
return (
|
| 79 |
+
{
|
| 80 |
+
"error": "No valid ISO-2 country codes were provided",
|
| 81 |
+
"unknown_codes": unknown,
|
| 82 |
+
},
|
| 83 |
+
state,
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
new_state = {
|
| 87 |
+
"version": _next_version(state),
|
| 88 |
+
"markers": list(state.get("markers", [])),
|
| 89 |
+
"highlights": list(state.get("highlights", [])),
|
| 90 |
+
"fly_to": state.get("fly_to"),
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
if action == "show":
|
| 94 |
+
new_state["markers"] = [_marker_payload(country) for country in resolved]
|
| 95 |
+
new_state["highlights"] = [_highlight_payload(country) for country in resolved]
|
| 96 |
+
new_state["fly_to"] = _fly_to_payload(resolved, zoom=zoom)
|
| 97 |
+
elif action in {"markers", "add_markers"}:
|
| 98 |
+
existing = {marker["iso2"] for marker in new_state["markers"]}
|
| 99 |
+
for country in resolved:
|
| 100 |
+
if country["iso2"] not in existing:
|
| 101 |
+
new_state["markers"].append(_marker_payload(country))
|
| 102 |
+
elif action == "highlight":
|
| 103 |
+
existing = {item["iso2"] for item in new_state["highlights"]}
|
| 104 |
+
for country in resolved:
|
| 105 |
+
if country["iso2"] not in existing:
|
| 106 |
+
new_state["highlights"].append(_highlight_payload(country))
|
| 107 |
+
elif action == "fly_to":
|
| 108 |
+
new_state["fly_to"] = _fly_to_payload(resolved, zoom=zoom)
|
| 109 |
+
else:
|
| 110 |
+
return (
|
| 111 |
+
{
|
| 112 |
+
"error": (
|
| 113 |
+
"Unknown action. Use show, markers, highlight, fly_to, or clear."
|
| 114 |
+
)
|
| 115 |
+
},
|
| 116 |
+
state,
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
result: dict[str, Any] = {
|
| 120 |
+
"status": "ok",
|
| 121 |
+
"action": action,
|
| 122 |
+
"countries": [
|
| 123 |
+
{
|
| 124 |
+
"iso2": country["iso2"],
|
| 125 |
+
"name": country["name"],
|
| 126 |
+
"label": country["label"],
|
| 127 |
+
}
|
| 128 |
+
for country in resolved
|
| 129 |
+
],
|
| 130 |
+
}
|
| 131 |
+
if unknown:
|
| 132 |
+
result["unknown_codes"] = unknown
|
| 133 |
+
return result, new_state
|
ui/inference/respond.py
CHANGED
|
@@ -21,6 +21,7 @@ def respond(
|
|
| 21 |
max_tokens,
|
| 22 |
temperature,
|
| 23 |
top_p,
|
|
|
|
| 24 |
hf_token: gr.OAuthToken,
|
| 25 |
):
|
| 26 |
"""
|
|
@@ -30,7 +31,11 @@ def respond(
|
|
| 30 |
|
| 31 |
user_content = multimodal_input_to_api_content(message)
|
| 32 |
if not user_content:
|
| 33 |
-
yield from yield_response(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
return
|
| 35 |
|
| 36 |
api_messages: list[dict[str, Any]] = [
|
|
@@ -53,14 +58,19 @@ def respond(
|
|
| 53 |
)
|
| 54 |
|
| 55 |
if tool_calls:
|
| 56 |
-
|
| 57 |
-
api_messages,
|
| 58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
continue
|
| 60 |
|
| 61 |
answer = content or reasoning
|
| 62 |
if answer:
|
| 63 |
-
yield from yield_response(ui_messages, answer)
|
| 64 |
return
|
| 65 |
|
| 66 |
# Retry once with a required tool call when the model returned nothing.
|
|
@@ -74,14 +84,19 @@ def respond(
|
|
| 74 |
tool_choice="required",
|
| 75 |
)
|
| 76 |
if tool_calls:
|
| 77 |
-
|
| 78 |
-
api_messages,
|
| 79 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
continue
|
| 81 |
|
| 82 |
answer = content or reasoning
|
| 83 |
if answer:
|
| 84 |
-
yield from yield_response(ui_messages, answer)
|
| 85 |
return
|
| 86 |
|
| 87 |
# Some providers return an empty first turn when tools are enabled.
|
|
@@ -95,12 +110,13 @@ def respond(
|
|
| 95 |
)
|
| 96 |
answer = content or reasoning
|
| 97 |
if answer:
|
| 98 |
-
yield from yield_response(ui_messages, answer)
|
| 99 |
return
|
| 100 |
|
| 101 |
yield from yield_response(
|
| 102 |
ui_messages,
|
| 103 |
"I could not generate a response. Please try again.",
|
|
|
|
| 104 |
)
|
| 105 |
return
|
| 106 |
|
|
@@ -108,10 +124,12 @@ def respond(
|
|
| 108 |
yield from yield_response(
|
| 109 |
ui_messages,
|
| 110 |
f"Sorry, something went wrong while generating a response: {exc}",
|
|
|
|
| 111 |
)
|
| 112 |
return
|
| 113 |
|
| 114 |
yield from yield_response(
|
| 115 |
ui_messages,
|
| 116 |
"I reached the maximum number of tool calls for this request.",
|
|
|
|
| 117 |
)
|
|
|
|
| 21 |
max_tokens,
|
| 22 |
temperature,
|
| 23 |
top_p,
|
| 24 |
+
globe_state: dict[str, Any],
|
| 25 |
hf_token: gr.OAuthToken,
|
| 26 |
):
|
| 27 |
"""
|
|
|
|
| 31 |
|
| 32 |
user_content = multimodal_input_to_api_content(message)
|
| 33 |
if not user_content:
|
| 34 |
+
yield from yield_response(
|
| 35 |
+
[],
|
| 36 |
+
"Please enter a message or attach a file.",
|
| 37 |
+
globe_state,
|
| 38 |
+
)
|
| 39 |
return
|
| 40 |
|
| 41 |
api_messages: list[dict[str, Any]] = [
|
|
|
|
| 58 |
)
|
| 59 |
|
| 60 |
if tool_calls:
|
| 61 |
+
for ui_msg, globe_state in execute_tool_calls(
|
| 62 |
+
api_messages,
|
| 63 |
+
ui_messages,
|
| 64 |
+
tool_calls,
|
| 65 |
+
content,
|
| 66 |
+
globe_state,
|
| 67 |
+
):
|
| 68 |
+
yield ui_msg, globe_state
|
| 69 |
continue
|
| 70 |
|
| 71 |
answer = content or reasoning
|
| 72 |
if answer:
|
| 73 |
+
yield from yield_response(ui_messages, answer, globe_state)
|
| 74 |
return
|
| 75 |
|
| 76 |
# Retry once with a required tool call when the model returned nothing.
|
|
|
|
| 84 |
tool_choice="required",
|
| 85 |
)
|
| 86 |
if tool_calls:
|
| 87 |
+
for ui_msg, globe_state in execute_tool_calls(
|
| 88 |
+
api_messages,
|
| 89 |
+
ui_messages,
|
| 90 |
+
tool_calls,
|
| 91 |
+
content,
|
| 92 |
+
globe_state,
|
| 93 |
+
):
|
| 94 |
+
yield ui_msg, globe_state
|
| 95 |
continue
|
| 96 |
|
| 97 |
answer = content or reasoning
|
| 98 |
if answer:
|
| 99 |
+
yield from yield_response(ui_messages, answer, globe_state)
|
| 100 |
return
|
| 101 |
|
| 102 |
# Some providers return an empty first turn when tools are enabled.
|
|
|
|
| 110 |
)
|
| 111 |
answer = content or reasoning
|
| 112 |
if answer:
|
| 113 |
+
yield from yield_response(ui_messages, answer, globe_state)
|
| 114 |
return
|
| 115 |
|
| 116 |
yield from yield_response(
|
| 117 |
ui_messages,
|
| 118 |
"I could not generate a response. Please try again.",
|
| 119 |
+
globe_state,
|
| 120 |
)
|
| 121 |
return
|
| 122 |
|
|
|
|
| 124 |
yield from yield_response(
|
| 125 |
ui_messages,
|
| 126 |
f"Sorry, something went wrong while generating a response: {exc}",
|
| 127 |
+
globe_state,
|
| 128 |
)
|
| 129 |
return
|
| 130 |
|
| 131 |
yield from yield_response(
|
| 132 |
ui_messages,
|
| 133 |
"I reached the maximum number of tool calls for this request.",
|
| 134 |
+
globe_state,
|
| 135 |
)
|
ui/inference/streaming.py
CHANGED
|
@@ -1,32 +1,42 @@
|
|
| 1 |
# ui/inference/streaming.py
|
| 2 |
from __future__ import annotations
|
| 3 |
|
|
|
|
|
|
|
| 4 |
from gradio import ChatMessage
|
| 5 |
|
| 6 |
|
| 7 |
-
def yield_streaming_string(text: str):
|
| 8 |
if not text:
|
| 9 |
-
yield ""
|
| 10 |
return
|
| 11 |
for index in range(len(text)):
|
| 12 |
-
yield text[: index + 1]
|
| 13 |
|
| 14 |
|
| 15 |
-
def yield_streaming_messages(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
messages = list(ui_messages)
|
| 17 |
if not text:
|
| 18 |
messages.append(ChatMessage(role="assistant", content=""))
|
| 19 |
-
yield messages
|
| 20 |
return
|
| 21 |
|
| 22 |
messages.append(ChatMessage(role="assistant", content=""))
|
| 23 |
for index in range(len(text)):
|
| 24 |
messages[-1] = ChatMessage(role="assistant", content=text[: index + 1])
|
| 25 |
-
yield messages
|
| 26 |
|
| 27 |
|
| 28 |
-
def yield_response(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
if ui_messages:
|
| 30 |
-
yield from yield_streaming_messages(ui_messages, text)
|
| 31 |
else:
|
| 32 |
-
yield from yield_streaming_string(text)
|
|
|
|
| 1 |
# ui/inference/streaming.py
|
| 2 |
from __future__ import annotations
|
| 3 |
|
| 4 |
+
from typing import Any
|
| 5 |
+
|
| 6 |
from gradio import ChatMessage
|
| 7 |
|
| 8 |
|
| 9 |
+
def yield_streaming_string(text: str, globe_state: dict[str, Any]):
|
| 10 |
if not text:
|
| 11 |
+
yield "", globe_state
|
| 12 |
return
|
| 13 |
for index in range(len(text)):
|
| 14 |
+
yield text[: index + 1], globe_state
|
| 15 |
|
| 16 |
|
| 17 |
+
def yield_streaming_messages(
|
| 18 |
+
ui_messages: list[ChatMessage],
|
| 19 |
+
text: str,
|
| 20 |
+
globe_state: dict[str, Any],
|
| 21 |
+
):
|
| 22 |
messages = list(ui_messages)
|
| 23 |
if not text:
|
| 24 |
messages.append(ChatMessage(role="assistant", content=""))
|
| 25 |
+
yield messages, globe_state
|
| 26 |
return
|
| 27 |
|
| 28 |
messages.append(ChatMessage(role="assistant", content=""))
|
| 29 |
for index in range(len(text)):
|
| 30 |
messages[-1] = ChatMessage(role="assistant", content=text[: index + 1])
|
| 31 |
+
yield messages, globe_state
|
| 32 |
|
| 33 |
|
| 34 |
+
def yield_response(
|
| 35 |
+
ui_messages: list[ChatMessage],
|
| 36 |
+
text: str,
|
| 37 |
+
globe_state: dict[str, Any],
|
| 38 |
+
):
|
| 39 |
if ui_messages:
|
| 40 |
+
yield from yield_streaming_messages(ui_messages, text, globe_state)
|
| 41 |
else:
|
| 42 |
+
yield from yield_streaming_string(text, globe_state)
|
ui/inference/tool_schemas/__init__.py
CHANGED
|
@@ -5,9 +5,11 @@ from typing import Any
|
|
| 5 |
from .get_economic_indicator import SCHEMA as get_economic_indicator
|
| 6 |
from .get_passport_stats import SCHEMA as get_passport_stats
|
| 7 |
from .get_visa_requirement import SCHEMA as get_visa_requirement
|
|
|
|
| 8 |
|
| 9 |
TOOL_SCHEMAS: list[dict[str, Any]] = [
|
| 10 |
get_economic_indicator,
|
| 11 |
get_visa_requirement,
|
| 12 |
get_passport_stats,
|
|
|
|
| 13 |
]
|
|
|
|
| 5 |
from .get_economic_indicator import SCHEMA as get_economic_indicator
|
| 6 |
from .get_passport_stats import SCHEMA as get_passport_stats
|
| 7 |
from .get_visa_requirement import SCHEMA as get_visa_requirement
|
| 8 |
+
from .update_globe import SCHEMA as update_globe
|
| 9 |
|
| 10 |
TOOL_SCHEMAS: list[dict[str, Any]] = [
|
| 11 |
get_economic_indicator,
|
| 12 |
get_visa_requirement,
|
| 13 |
get_passport_stats,
|
| 14 |
+
update_globe,
|
| 15 |
]
|
ui/inference/tool_schemas/update_globe.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Any
|
| 4 |
+
|
| 5 |
+
SCHEMA: dict[str, Any] = {
|
| 6 |
+
"type": "function",
|
| 7 |
+
"function": {
|
| 8 |
+
"name": "update_globe",
|
| 9 |
+
"description": (
|
| 10 |
+
"Update the interactive 3D globe beside the chat. Use this when "
|
| 11 |
+
"presenting destination countries, comparing options, or guiding "
|
| 12 |
+
"the user geographically. Use ISO-2 country codes (e.g. US, CA, DE, IN, AU)."
|
| 13 |
+
),
|
| 14 |
+
"parameters": {
|
| 15 |
+
"type": "object",
|
| 16 |
+
"properties": {
|
| 17 |
+
"action": {
|
| 18 |
+
"type": "string",
|
| 19 |
+
"enum": ["show", "markers", "highlight", "fly_to", "clear"],
|
| 20 |
+
"description": (
|
| 21 |
+
"show: replace globe overlays and fly to all countries "
|
| 22 |
+
"(best for a shortlist). markers: add pin markers. "
|
| 23 |
+
"highlight: add glowing highlights. fly_to: move the camera. "
|
| 24 |
+
"clear: remove markers and highlights."
|
| 25 |
+
),
|
| 26 |
+
},
|
| 27 |
+
"countries": {
|
| 28 |
+
"type": "array",
|
| 29 |
+
"items": {"type": "string"},
|
| 30 |
+
"description": "ISO-2 country codes to show on the globe.",
|
| 31 |
+
},
|
| 32 |
+
"labels": {
|
| 33 |
+
"type": "array",
|
| 34 |
+
"items": {"type": "string"},
|
| 35 |
+
"description": (
|
| 36 |
+
"Optional labels for markers, parallel to countries "
|
| 37 |
+
"(e.g. 'Skilled worker visa')."
|
| 38 |
+
),
|
| 39 |
+
},
|
| 40 |
+
"zoom": {
|
| 41 |
+
"type": "number",
|
| 42 |
+
"description": "Optional zoom level when flying to a single country.",
|
| 43 |
+
},
|
| 44 |
+
},
|
| 45 |
+
"required": ["action"],
|
| 46 |
+
},
|
| 47 |
+
},
|
| 48 |
+
}
|
ui/inference/tools.py
CHANGED
|
@@ -9,6 +9,7 @@ from gradio import ChatMessage
|
|
| 9 |
|
| 10 |
from apis.passport_visa import passport_stats, visa_requirement
|
| 11 |
from apis.world_bank import get_indicator_series
|
|
|
|
| 12 |
|
| 13 |
from .messages import assistant_message_dict
|
| 14 |
|
|
@@ -19,30 +20,50 @@ def truncate(text: str, limit: int = 4000) -> str:
|
|
| 19 |
return text[:limit] + "\n… (truncated)"
|
| 20 |
|
| 21 |
|
| 22 |
-
def run_tool(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
try:
|
| 24 |
args = json.loads(arguments or "{}")
|
| 25 |
-
result = _run_tool(name, args)
|
| 26 |
-
return json.dumps(result, default=str)
|
| 27 |
except Exception as exc:
|
| 28 |
-
return json.dumps({"error": str(exc)})
|
| 29 |
|
| 30 |
|
| 31 |
-
def _run_tool(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
if name == "get_economic_indicator":
|
| 33 |
-
return
|
| 34 |
-
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
| 36 |
)
|
| 37 |
if name == "get_visa_requirement":
|
| 38 |
-
return
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
| 41 |
)
|
| 42 |
if name == "get_passport_stats":
|
| 43 |
-
return passport_stats(passport_iso2=args["passport_iso2"])
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
-
return {"error": f"Unknown tool: {name}"}
|
| 46 |
|
| 47 |
|
| 48 |
def execute_tool_calls(
|
|
@@ -50,6 +71,7 @@ def execute_tool_calls(
|
|
| 50 |
ui_messages: list[ChatMessage],
|
| 51 |
tool_calls: list[dict[str, Any]],
|
| 52 |
content: str,
|
|
|
|
| 53 |
):
|
| 54 |
api_messages.append(assistant_message_dict(content, tool_calls))
|
| 55 |
|
|
@@ -71,9 +93,13 @@ def execute_tool_calls(
|
|
| 71 |
},
|
| 72 |
)
|
| 73 |
)
|
| 74 |
-
yield ui_messages
|
| 75 |
|
| 76 |
-
result = run_tool(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
duration = time.monotonic() - started
|
| 78 |
|
| 79 |
ui_messages[-1] = ChatMessage(
|
|
@@ -89,7 +115,7 @@ def execute_tool_calls(
|
|
| 89 |
"duration": duration,
|
| 90 |
},
|
| 91 |
)
|
| 92 |
-
yield ui_messages
|
| 93 |
|
| 94 |
api_messages.append(
|
| 95 |
{
|
|
|
|
| 9 |
|
| 10 |
from apis.passport_visa import passport_stats, visa_requirement
|
| 11 |
from apis.world_bank import get_indicator_series
|
| 12 |
+
from ui.globe_commands import apply_update_globe
|
| 13 |
|
| 14 |
from .messages import assistant_message_dict
|
| 15 |
|
|
|
|
| 20 |
return text[:limit] + "\n… (truncated)"
|
| 21 |
|
| 22 |
|
| 23 |
+
def run_tool(
|
| 24 |
+
name: str,
|
| 25 |
+
arguments: str,
|
| 26 |
+
*,
|
| 27 |
+
globe_state: dict[str, Any] | None = None,
|
| 28 |
+
) -> tuple[str, dict[str, Any] | None]:
|
| 29 |
try:
|
| 30 |
args = json.loads(arguments or "{}")
|
| 31 |
+
result, updated_globe = _run_tool(name, args, globe_state=globe_state)
|
| 32 |
+
return json.dumps(result, default=str), updated_globe
|
| 33 |
except Exception as exc:
|
| 34 |
+
return json.dumps({"error": str(exc)}), globe_state
|
| 35 |
|
| 36 |
|
| 37 |
+
def _run_tool(
|
| 38 |
+
name: str,
|
| 39 |
+
args: dict[str, Any],
|
| 40 |
+
*,
|
| 41 |
+
globe_state: dict[str, Any] | None = None,
|
| 42 |
+
) -> tuple[Any, dict[str, Any] | None]:
|
| 43 |
if name == "get_economic_indicator":
|
| 44 |
+
return (
|
| 45 |
+
get_indicator_series(
|
| 46 |
+
indicator=args["indicator"],
|
| 47 |
+
country=args["country"],
|
| 48 |
+
),
|
| 49 |
+
globe_state,
|
| 50 |
)
|
| 51 |
if name == "get_visa_requirement":
|
| 52 |
+
return (
|
| 53 |
+
visa_requirement(
|
| 54 |
+
passport_iso2=args["passport_iso2"],
|
| 55 |
+
destination_iso2=args["destination_iso2"],
|
| 56 |
+
),
|
| 57 |
+
globe_state,
|
| 58 |
)
|
| 59 |
if name == "get_passport_stats":
|
| 60 |
+
return passport_stats(passport_iso2=args["passport_iso2"]), globe_state
|
| 61 |
+
if name == "update_globe":
|
| 62 |
+
if globe_state is None:
|
| 63 |
+
return {"error": "Globe state is unavailable"}, None
|
| 64 |
+
return apply_update_globe(globe_state, args)
|
| 65 |
|
| 66 |
+
return {"error": f"Unknown tool: {name}"}, globe_state
|
| 67 |
|
| 68 |
|
| 69 |
def execute_tool_calls(
|
|
|
|
| 71 |
ui_messages: list[ChatMessage],
|
| 72 |
tool_calls: list[dict[str, Any]],
|
| 73 |
content: str,
|
| 74 |
+
globe_state: dict[str, Any],
|
| 75 |
):
|
| 76 |
api_messages.append(assistant_message_dict(content, tool_calls))
|
| 77 |
|
|
|
|
| 93 |
},
|
| 94 |
)
|
| 95 |
)
|
| 96 |
+
yield ui_messages, globe_state
|
| 97 |
|
| 98 |
+
result, globe_state = run_tool(
|
| 99 |
+
tool_name,
|
| 100 |
+
tool_args,
|
| 101 |
+
globe_state=globe_state,
|
| 102 |
+
)
|
| 103 |
duration = time.monotonic() - started
|
| 104 |
|
| 105 |
ui_messages[-1] = ChatMessage(
|
|
|
|
| 115 |
"duration": duration,
|
| 116 |
},
|
| 117 |
)
|
| 118 |
+
yield ui_messages, globe_state
|
| 119 |
|
| 120 |
api_messages.append(
|
| 121 |
{
|