holosphere 2.0.0-alpha7 → 2.0.0-alpha8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/holosphere.cjs +1 -1
- package/dist/esm/holosphere.js +1 -1
- package/dist/{index-d6f4RJBM.js → index-4XHHKe6S.js} +356 -58
- package/dist/index-4XHHKe6S.js.map +1 -0
- package/dist/{index-jmTHEbR2.js → index-BjP1TXGz.js} +2 -2
- package/dist/{index-jmTHEbR2.js.map → index-BjP1TXGz.js.map} +1 -1
- package/dist/{index-C-IlLYlk.cjs → index-CKffQDmQ.cjs} +2 -2
- package/dist/{index-C-IlLYlk.cjs.map → index-CKffQDmQ.cjs.map} +1 -1
- package/dist/index-Dz5kOZMI.cjs +5 -0
- package/dist/index-Dz5kOZMI.cjs.map +1 -0
- package/dist/{indexeddb-storage-a8GipaDr.cjs → indexeddb-storage-DD7EFBVc.cjs} +2 -2
- package/dist/{indexeddb-storage-a8GipaDr.cjs.map → indexeddb-storage-DD7EFBVc.cjs.map} +1 -1
- package/dist/{indexeddb-storage-D8kOl0oK.js → indexeddb-storage-lExjjFlV.js} +2 -2
- package/dist/{indexeddb-storage-D8kOl0oK.js.map → indexeddb-storage-lExjjFlV.js.map} +1 -1
- package/dist/{memory-storage-DBQK622V.js → memory-storage-C68adso2.js} +2 -2
- package/dist/{memory-storage-DBQK622V.js.map → memory-storage-C68adso2.js.map} +1 -1
- package/dist/{memory-storage-gfRovk2O.cjs → memory-storage-DD_6yyXT.cjs} +2 -2
- package/dist/{memory-storage-gfRovk2O.cjs.map → memory-storage-DD_6yyXT.cjs.map} +1 -1
- package/dist/{secp256k1-BCAPF45D.cjs → secp256k1-DYELiqgx.cjs} +2 -2
- package/dist/{secp256k1-BCAPF45D.cjs.map → secp256k1-DYELiqgx.cjs.map} +1 -1
- package/dist/{secp256k1-DYm_CMqW.js → secp256k1-OM8siPyy.js} +2 -2
- package/dist/{secp256k1-DYm_CMqW.js.map → secp256k1-OM8siPyy.js.map} +1 -1
- package/examples/holosphere-widget.js +1242 -0
- package/examples/widget-demo.html +274 -0
- package/examples/widget.html +703 -0
- package/package.json +3 -1
- package/src/cdn-entry.js +22 -0
- package/src/contracts/queries.js +16 -1
- package/src/core/holosphere.js +2 -2
- package/src/crypto/nostr-utils.js +36 -2
- package/src/federation/handshake.js +16 -4
- package/src/index.js +16 -2
- package/src/storage/backends/gundb-backend.js +293 -9
- package/src/storage/gun-wrapper.js +64 -16
- package/src/storage/nostr-async.js +40 -25
- package/src/storage/unified-storage.js +31 -1
- package/vite.config.cdn.js +60 -0
- package/dist/index-Bvwyvd0T.cjs +0 -5
- package/dist/index-Bvwyvd0T.cjs.map +0 -1
- package/dist/index-d6f4RJBM.js.map +0 -1
|
@@ -0,0 +1,1242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HoloSphere Visual Widget
|
|
3
|
+
*
|
|
4
|
+
* A floating widget that provides:
|
|
5
|
+
* - Zoomable H3 hexagon map
|
|
6
|
+
* - Holonic data explorer
|
|
7
|
+
* - Real-time data visualization
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* <script src="holosphere.min.js"></script>
|
|
11
|
+
* <script src="holosphere-widget.js"></script>
|
|
12
|
+
* <script>
|
|
13
|
+
* HoloSphereWidget.init({ appName: 'my-app' });
|
|
14
|
+
* </script>
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
(function(global) {
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
// Widget state
|
|
21
|
+
let hs = null;
|
|
22
|
+
let map = null;
|
|
23
|
+
let currentHolon = null;
|
|
24
|
+
let currentResolution = 9;
|
|
25
|
+
let hexagonLayers = {};
|
|
26
|
+
let selectedHexLayer = null;
|
|
27
|
+
let isOpen = false;
|
|
28
|
+
let config = {};
|
|
29
|
+
|
|
30
|
+
// H3 resolution to zoom level mapping
|
|
31
|
+
const resolutionToZoom = {
|
|
32
|
+
0: 2, 1: 3, 2: 4, 3: 5, 4: 6, 5: 7,
|
|
33
|
+
6: 8, 7: 9, 8: 10, 9: 11, 10: 12, 11: 13,
|
|
34
|
+
12: 14, 13: 15, 14: 16, 15: 17
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const zoomToResolution = Object.fromEntries(
|
|
38
|
+
Object.entries(resolutionToZoom).map(([k, v]) => [v, parseInt(k)])
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// Color palette for hexagons
|
|
42
|
+
const colors = {
|
|
43
|
+
empty: 'rgba(102, 126, 234, 0.2)',
|
|
44
|
+
hasData: 'rgba(102, 126, 234, 0.6)',
|
|
45
|
+
selected: 'rgba(118, 75, 162, 0.8)',
|
|
46
|
+
hover: 'rgba(102, 126, 234, 0.4)',
|
|
47
|
+
stroke: '#667eea',
|
|
48
|
+
strokeSelected: '#764ba2'
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Widget HTML template
|
|
52
|
+
const widgetHTML = `
|
|
53
|
+
<div id="hs-widget-button" class="hs-widget-button">
|
|
54
|
+
<svg viewBox="0 0 100 100" width="40" height="40">
|
|
55
|
+
<polygon points="50,5 95,27.5 95,72.5 50,95 5,72.5 5,27.5"
|
|
56
|
+
fill="none" stroke="currentColor" stroke-width="4"/>
|
|
57
|
+
<polygon points="50,25 72.5,37.5 72.5,62.5 50,75 27.5,62.5 27.5,37.5"
|
|
58
|
+
fill="currentColor" opacity="0.3"/>
|
|
59
|
+
<circle cx="50" cy="50" r="8" fill="currentColor"/>
|
|
60
|
+
</svg>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div id="hs-widget-modal" class="hs-widget-modal">
|
|
64
|
+
<div class="hs-widget-container">
|
|
65
|
+
<div class="hs-widget-header">
|
|
66
|
+
<h2>
|
|
67
|
+
<svg viewBox="0 0 100 100" width="24" height="24" style="vertical-align: middle; margin-right: 8px;">
|
|
68
|
+
<polygon points="50,5 95,27.5 95,72.5 50,95 5,72.5 5,27.5"
|
|
69
|
+
fill="none" stroke="currentColor" stroke-width="6"/>
|
|
70
|
+
</svg>
|
|
71
|
+
HoloSphere Explorer
|
|
72
|
+
</h2>
|
|
73
|
+
<button id="hs-close-btn" class="hs-close-btn">×</button>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<div class="hs-widget-body">
|
|
77
|
+
<div class="hs-map-panel">
|
|
78
|
+
<div id="hs-map"></div>
|
|
79
|
+
<div class="hs-map-controls">
|
|
80
|
+
<div class="hs-resolution-control">
|
|
81
|
+
<label>Resolution: <span id="hs-resolution-value">9</span></label>
|
|
82
|
+
<input type="range" id="hs-resolution-slider" min="0" max="15" value="9">
|
|
83
|
+
</div>
|
|
84
|
+
<div class="hs-coords-display" id="hs-coords">
|
|
85
|
+
Click map to select holon
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<div class="hs-explorer-panel">
|
|
91
|
+
<div class="hs-tabs">
|
|
92
|
+
<button class="hs-tab active" data-tab="data">Data</button>
|
|
93
|
+
<button class="hs-tab" data-tab="hierarchy">Hierarchy</button>
|
|
94
|
+
<button class="hs-tab" data-tab="lenses">Lenses</button>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<div class="hs-tab-content active" id="hs-tab-data">
|
|
98
|
+
<div class="hs-holon-info">
|
|
99
|
+
<div class="hs-holon-id" id="hs-current-holon">No holon selected</div>
|
|
100
|
+
</div>
|
|
101
|
+
<div class="hs-lens-selector">
|
|
102
|
+
<select id="hs-lens-select">
|
|
103
|
+
<option value="">Select lens...</option>
|
|
104
|
+
</select>
|
|
105
|
+
<button id="hs-add-lens-btn" class="hs-btn-small">+ Add Lens</button>
|
|
106
|
+
</div>
|
|
107
|
+
<div class="hs-data-list" id="hs-data-list">
|
|
108
|
+
<div class="hs-empty-state">Select a holon on the map to view data</div>
|
|
109
|
+
</div>
|
|
110
|
+
<div class="hs-data-actions">
|
|
111
|
+
<button id="hs-write-btn" class="hs-btn" disabled>Write Data</button>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div class="hs-tab-content" id="hs-tab-hierarchy">
|
|
116
|
+
<div class="hs-hierarchy-view" id="hs-hierarchy">
|
|
117
|
+
<div class="hs-empty-state">Select a holon to explore hierarchy</div>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<div class="hs-tab-content" id="hs-tab-lenses">
|
|
122
|
+
<div class="hs-lenses-list" id="hs-lenses-list">
|
|
123
|
+
<div class="hs-empty-state">Select a holon to view available lenses</div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<div class="hs-widget-footer">
|
|
130
|
+
<span class="hs-status" id="hs-status">
|
|
131
|
+
<span class="hs-status-dot"></span>
|
|
132
|
+
<span id="hs-status-text">Initializing...</span>
|
|
133
|
+
</span>
|
|
134
|
+
<span class="hs-metrics" id="hs-metrics"></span>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<!-- Write Data Modal -->
|
|
140
|
+
<div id="hs-write-modal" class="hs-write-modal">
|
|
141
|
+
<div class="hs-write-container">
|
|
142
|
+
<h3>Write Data to Holon</h3>
|
|
143
|
+
<div class="hs-form-group">
|
|
144
|
+
<label>Holon ID</label>
|
|
145
|
+
<input type="text" id="hs-write-holon" readonly>
|
|
146
|
+
</div>
|
|
147
|
+
<div class="hs-form-group">
|
|
148
|
+
<label>Lens</label>
|
|
149
|
+
<input type="text" id="hs-write-lens" placeholder="e.g., events, notes, places">
|
|
150
|
+
</div>
|
|
151
|
+
<div class="hs-form-group">
|
|
152
|
+
<label>Data (JSON)</label>
|
|
153
|
+
<textarea id="hs-write-data" placeholder='{"title": "My Data", "description": "..."}'></textarea>
|
|
154
|
+
</div>
|
|
155
|
+
<div class="hs-form-actions">
|
|
156
|
+
<button id="hs-write-cancel" class="hs-btn-secondary">Cancel</button>
|
|
157
|
+
<button id="hs-write-submit" class="hs-btn">Write</button>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
`;
|
|
162
|
+
|
|
163
|
+
// Widget CSS
|
|
164
|
+
const widgetCSS = `
|
|
165
|
+
.hs-widget-button {
|
|
166
|
+
position: fixed;
|
|
167
|
+
bottom: 20px;
|
|
168
|
+
right: 20px;
|
|
169
|
+
width: 60px;
|
|
170
|
+
height: 60px;
|
|
171
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
172
|
+
border-radius: 50%;
|
|
173
|
+
cursor: pointer;
|
|
174
|
+
display: flex;
|
|
175
|
+
align-items: center;
|
|
176
|
+
justify-content: center;
|
|
177
|
+
color: white;
|
|
178
|
+
box-shadow: 0 4px 20px rgba(102, 126, 234, 0.5);
|
|
179
|
+
transition: transform 0.3s, box-shadow 0.3s;
|
|
180
|
+
z-index: 999998;
|
|
181
|
+
}
|
|
182
|
+
.hs-widget-button:hover {
|
|
183
|
+
transform: scale(1.1);
|
|
184
|
+
box-shadow: 0 6px 30px rgba(102, 126, 234, 0.7);
|
|
185
|
+
}
|
|
186
|
+
.hs-widget-button.active {
|
|
187
|
+
transform: rotate(30deg);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.hs-widget-modal {
|
|
191
|
+
position: fixed;
|
|
192
|
+
top: 0;
|
|
193
|
+
left: 0;
|
|
194
|
+
width: 100%;
|
|
195
|
+
height: 100%;
|
|
196
|
+
background: rgba(0, 0, 0, 0.5);
|
|
197
|
+
display: none;
|
|
198
|
+
align-items: center;
|
|
199
|
+
justify-content: center;
|
|
200
|
+
z-index: 999999;
|
|
201
|
+
opacity: 0;
|
|
202
|
+
transition: opacity 0.3s;
|
|
203
|
+
}
|
|
204
|
+
.hs-widget-modal.open {
|
|
205
|
+
display: flex;
|
|
206
|
+
opacity: 1;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.hs-widget-container {
|
|
210
|
+
width: 95%;
|
|
211
|
+
max-width: 1200px;
|
|
212
|
+
height: 85vh;
|
|
213
|
+
background: white;
|
|
214
|
+
border-radius: 16px;
|
|
215
|
+
display: flex;
|
|
216
|
+
flex-direction: column;
|
|
217
|
+
overflow: hidden;
|
|
218
|
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
219
|
+
transform: translateY(20px);
|
|
220
|
+
transition: transform 0.3s;
|
|
221
|
+
}
|
|
222
|
+
.hs-widget-modal.open .hs-widget-container {
|
|
223
|
+
transform: translateY(0);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.hs-widget-header {
|
|
227
|
+
padding: 16px 24px;
|
|
228
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
229
|
+
color: white;
|
|
230
|
+
display: flex;
|
|
231
|
+
justify-content: space-between;
|
|
232
|
+
align-items: center;
|
|
233
|
+
}
|
|
234
|
+
.hs-widget-header h2 {
|
|
235
|
+
margin: 0;
|
|
236
|
+
font-size: 1.3em;
|
|
237
|
+
font-weight: 600;
|
|
238
|
+
}
|
|
239
|
+
.hs-close-btn {
|
|
240
|
+
background: none;
|
|
241
|
+
border: none;
|
|
242
|
+
color: white;
|
|
243
|
+
font-size: 28px;
|
|
244
|
+
cursor: pointer;
|
|
245
|
+
width: 40px;
|
|
246
|
+
height: 40px;
|
|
247
|
+
border-radius: 50%;
|
|
248
|
+
transition: background 0.2s;
|
|
249
|
+
}
|
|
250
|
+
.hs-close-btn:hover {
|
|
251
|
+
background: rgba(255,255,255,0.2);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.hs-widget-body {
|
|
255
|
+
flex: 1;
|
|
256
|
+
display: flex;
|
|
257
|
+
overflow: hidden;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.hs-map-panel {
|
|
261
|
+
flex: 1;
|
|
262
|
+
display: flex;
|
|
263
|
+
flex-direction: column;
|
|
264
|
+
border-right: 1px solid #e0e0e0;
|
|
265
|
+
}
|
|
266
|
+
#hs-map {
|
|
267
|
+
flex: 1;
|
|
268
|
+
min-height: 300px;
|
|
269
|
+
}
|
|
270
|
+
.hs-map-controls {
|
|
271
|
+
padding: 12px 16px;
|
|
272
|
+
background: #f8f9fa;
|
|
273
|
+
border-top: 1px solid #e0e0e0;
|
|
274
|
+
}
|
|
275
|
+
.hs-resolution-control {
|
|
276
|
+
display: flex;
|
|
277
|
+
align-items: center;
|
|
278
|
+
gap: 12px;
|
|
279
|
+
margin-bottom: 8px;
|
|
280
|
+
}
|
|
281
|
+
.hs-resolution-control label {
|
|
282
|
+
font-size: 13px;
|
|
283
|
+
color: #666;
|
|
284
|
+
min-width: 100px;
|
|
285
|
+
}
|
|
286
|
+
.hs-resolution-control input[type="range"] {
|
|
287
|
+
flex: 1;
|
|
288
|
+
accent-color: #667eea;
|
|
289
|
+
}
|
|
290
|
+
.hs-coords-display {
|
|
291
|
+
font-family: monospace;
|
|
292
|
+
font-size: 12px;
|
|
293
|
+
color: #666;
|
|
294
|
+
padding: 8px;
|
|
295
|
+
background: white;
|
|
296
|
+
border-radius: 4px;
|
|
297
|
+
border: 1px solid #e0e0e0;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.hs-explorer-panel {
|
|
301
|
+
width: 400px;
|
|
302
|
+
display: flex;
|
|
303
|
+
flex-direction: column;
|
|
304
|
+
background: #fafafa;
|
|
305
|
+
}
|
|
306
|
+
.hs-tabs {
|
|
307
|
+
display: flex;
|
|
308
|
+
background: white;
|
|
309
|
+
border-bottom: 1px solid #e0e0e0;
|
|
310
|
+
}
|
|
311
|
+
.hs-tab {
|
|
312
|
+
flex: 1;
|
|
313
|
+
padding: 12px;
|
|
314
|
+
background: none;
|
|
315
|
+
border: none;
|
|
316
|
+
cursor: pointer;
|
|
317
|
+
font-size: 13px;
|
|
318
|
+
font-weight: 500;
|
|
319
|
+
color: #666;
|
|
320
|
+
border-bottom: 2px solid transparent;
|
|
321
|
+
transition: all 0.2s;
|
|
322
|
+
}
|
|
323
|
+
.hs-tab:hover {
|
|
324
|
+
color: #667eea;
|
|
325
|
+
}
|
|
326
|
+
.hs-tab.active {
|
|
327
|
+
color: #667eea;
|
|
328
|
+
border-bottom-color: #667eea;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.hs-tab-content {
|
|
332
|
+
display: none;
|
|
333
|
+
flex: 1;
|
|
334
|
+
flex-direction: column;
|
|
335
|
+
overflow: hidden;
|
|
336
|
+
}
|
|
337
|
+
.hs-tab-content.active {
|
|
338
|
+
display: flex;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.hs-holon-info {
|
|
342
|
+
padding: 12px 16px;
|
|
343
|
+
background: white;
|
|
344
|
+
border-bottom: 1px solid #e0e0e0;
|
|
345
|
+
}
|
|
346
|
+
.hs-holon-id {
|
|
347
|
+
font-family: monospace;
|
|
348
|
+
font-size: 13px;
|
|
349
|
+
color: #333;
|
|
350
|
+
word-break: break-all;
|
|
351
|
+
padding: 8px;
|
|
352
|
+
background: #f0f0f0;
|
|
353
|
+
border-radius: 4px;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.hs-lens-selector {
|
|
357
|
+
padding: 12px 16px;
|
|
358
|
+
display: flex;
|
|
359
|
+
gap: 8px;
|
|
360
|
+
background: white;
|
|
361
|
+
border-bottom: 1px solid #e0e0e0;
|
|
362
|
+
}
|
|
363
|
+
.hs-lens-selector select {
|
|
364
|
+
flex: 1;
|
|
365
|
+
padding: 8px 12px;
|
|
366
|
+
border: 1px solid #ddd;
|
|
367
|
+
border-radius: 6px;
|
|
368
|
+
font-size: 13px;
|
|
369
|
+
}
|
|
370
|
+
.hs-btn-small {
|
|
371
|
+
padding: 8px 12px;
|
|
372
|
+
background: #667eea;
|
|
373
|
+
color: white;
|
|
374
|
+
border: none;
|
|
375
|
+
border-radius: 6px;
|
|
376
|
+
cursor: pointer;
|
|
377
|
+
font-size: 12px;
|
|
378
|
+
white-space: nowrap;
|
|
379
|
+
}
|
|
380
|
+
.hs-btn-small:hover {
|
|
381
|
+
background: #5a6fd6;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.hs-data-list {
|
|
385
|
+
flex: 1;
|
|
386
|
+
overflow-y: auto;
|
|
387
|
+
padding: 12px 16px;
|
|
388
|
+
}
|
|
389
|
+
.hs-data-item {
|
|
390
|
+
background: white;
|
|
391
|
+
border-radius: 8px;
|
|
392
|
+
padding: 12px;
|
|
393
|
+
margin-bottom: 8px;
|
|
394
|
+
border: 1px solid #e0e0e0;
|
|
395
|
+
cursor: pointer;
|
|
396
|
+
transition: border-color 0.2s;
|
|
397
|
+
}
|
|
398
|
+
.hs-data-item:hover {
|
|
399
|
+
border-color: #667eea;
|
|
400
|
+
}
|
|
401
|
+
.hs-data-item-title {
|
|
402
|
+
font-weight: 500;
|
|
403
|
+
color: #333;
|
|
404
|
+
margin-bottom: 4px;
|
|
405
|
+
}
|
|
406
|
+
.hs-data-item-meta {
|
|
407
|
+
font-size: 11px;
|
|
408
|
+
color: #999;
|
|
409
|
+
}
|
|
410
|
+
.hs-data-item-preview {
|
|
411
|
+
font-size: 12px;
|
|
412
|
+
color: #666;
|
|
413
|
+
margin-top: 8px;
|
|
414
|
+
padding-top: 8px;
|
|
415
|
+
border-top: 1px solid #f0f0f0;
|
|
416
|
+
max-height: 60px;
|
|
417
|
+
overflow: hidden;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.hs-empty-state {
|
|
421
|
+
text-align: center;
|
|
422
|
+
color: #999;
|
|
423
|
+
padding: 40px 20px;
|
|
424
|
+
font-size: 14px;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.hs-data-actions {
|
|
428
|
+
padding: 12px 16px;
|
|
429
|
+
background: white;
|
|
430
|
+
border-top: 1px solid #e0e0e0;
|
|
431
|
+
}
|
|
432
|
+
.hs-btn {
|
|
433
|
+
width: 100%;
|
|
434
|
+
padding: 12px;
|
|
435
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
436
|
+
color: white;
|
|
437
|
+
border: none;
|
|
438
|
+
border-radius: 8px;
|
|
439
|
+
cursor: pointer;
|
|
440
|
+
font-size: 14px;
|
|
441
|
+
font-weight: 500;
|
|
442
|
+
transition: opacity 0.2s;
|
|
443
|
+
}
|
|
444
|
+
.hs-btn:hover:not(:disabled) {
|
|
445
|
+
opacity: 0.9;
|
|
446
|
+
}
|
|
447
|
+
.hs-btn:disabled {
|
|
448
|
+
opacity: 0.5;
|
|
449
|
+
cursor: not-allowed;
|
|
450
|
+
}
|
|
451
|
+
.hs-btn-secondary {
|
|
452
|
+
background: #6c757d;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
.hs-hierarchy-view {
|
|
456
|
+
flex: 1;
|
|
457
|
+
overflow-y: auto;
|
|
458
|
+
padding: 16px;
|
|
459
|
+
}
|
|
460
|
+
.hs-hierarchy-item {
|
|
461
|
+
padding: 10px 12px;
|
|
462
|
+
margin: 4px 0;
|
|
463
|
+
background: white;
|
|
464
|
+
border-radius: 6px;
|
|
465
|
+
border: 1px solid #e0e0e0;
|
|
466
|
+
cursor: pointer;
|
|
467
|
+
transition: all 0.2s;
|
|
468
|
+
display: flex;
|
|
469
|
+
align-items: center;
|
|
470
|
+
gap: 8px;
|
|
471
|
+
}
|
|
472
|
+
.hs-hierarchy-item:hover {
|
|
473
|
+
border-color: #667eea;
|
|
474
|
+
background: #f8f9ff;
|
|
475
|
+
}
|
|
476
|
+
.hs-hierarchy-item.current {
|
|
477
|
+
border-color: #764ba2;
|
|
478
|
+
background: linear-gradient(135deg, rgba(102,126,234,0.1), rgba(118,75,162,0.1));
|
|
479
|
+
}
|
|
480
|
+
.hs-hierarchy-item .resolution {
|
|
481
|
+
font-size: 10px;
|
|
482
|
+
background: #667eea;
|
|
483
|
+
color: white;
|
|
484
|
+
padding: 2px 6px;
|
|
485
|
+
border-radius: 10px;
|
|
486
|
+
}
|
|
487
|
+
.hs-hierarchy-item .holon-id {
|
|
488
|
+
font-family: monospace;
|
|
489
|
+
font-size: 12px;
|
|
490
|
+
color: #666;
|
|
491
|
+
flex: 1;
|
|
492
|
+
overflow: hidden;
|
|
493
|
+
text-overflow: ellipsis;
|
|
494
|
+
}
|
|
495
|
+
.hs-hierarchy-item .data-count {
|
|
496
|
+
font-size: 11px;
|
|
497
|
+
color: #999;
|
|
498
|
+
}
|
|
499
|
+
.hs-hierarchy-section {
|
|
500
|
+
margin-bottom: 16px;
|
|
501
|
+
}
|
|
502
|
+
.hs-hierarchy-section-title {
|
|
503
|
+
font-size: 11px;
|
|
504
|
+
text-transform: uppercase;
|
|
505
|
+
color: #999;
|
|
506
|
+
margin-bottom: 8px;
|
|
507
|
+
padding-left: 4px;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
.hs-lenses-list {
|
|
511
|
+
flex: 1;
|
|
512
|
+
overflow-y: auto;
|
|
513
|
+
padding: 16px;
|
|
514
|
+
}
|
|
515
|
+
.hs-lens-item {
|
|
516
|
+
background: white;
|
|
517
|
+
border-radius: 8px;
|
|
518
|
+
padding: 12px;
|
|
519
|
+
margin-bottom: 8px;
|
|
520
|
+
border: 1px solid #e0e0e0;
|
|
521
|
+
display: flex;
|
|
522
|
+
justify-content: space-between;
|
|
523
|
+
align-items: center;
|
|
524
|
+
}
|
|
525
|
+
.hs-lens-name {
|
|
526
|
+
font-weight: 500;
|
|
527
|
+
color: #333;
|
|
528
|
+
}
|
|
529
|
+
.hs-lens-count {
|
|
530
|
+
font-size: 12px;
|
|
531
|
+
color: #999;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
.hs-widget-footer {
|
|
535
|
+
padding: 12px 24px;
|
|
536
|
+
background: #f8f9fa;
|
|
537
|
+
border-top: 1px solid #e0e0e0;
|
|
538
|
+
display: flex;
|
|
539
|
+
justify-content: space-between;
|
|
540
|
+
align-items: center;
|
|
541
|
+
font-size: 12px;
|
|
542
|
+
}
|
|
543
|
+
.hs-status {
|
|
544
|
+
display: flex;
|
|
545
|
+
align-items: center;
|
|
546
|
+
gap: 8px;
|
|
547
|
+
}
|
|
548
|
+
.hs-status-dot {
|
|
549
|
+
width: 8px;
|
|
550
|
+
height: 8px;
|
|
551
|
+
border-radius: 50%;
|
|
552
|
+
background: #ffc107;
|
|
553
|
+
}
|
|
554
|
+
.hs-status-dot.connected {
|
|
555
|
+
background: #28a745;
|
|
556
|
+
animation: pulse 2s infinite;
|
|
557
|
+
}
|
|
558
|
+
@keyframes pulse {
|
|
559
|
+
0%, 100% { opacity: 1; }
|
|
560
|
+
50% { opacity: 0.5; }
|
|
561
|
+
}
|
|
562
|
+
.hs-metrics {
|
|
563
|
+
color: #666;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/* Write Modal */
|
|
567
|
+
.hs-write-modal {
|
|
568
|
+
position: fixed;
|
|
569
|
+
top: 0;
|
|
570
|
+
left: 0;
|
|
571
|
+
width: 100%;
|
|
572
|
+
height: 100%;
|
|
573
|
+
background: rgba(0,0,0,0.5);
|
|
574
|
+
display: none;
|
|
575
|
+
align-items: center;
|
|
576
|
+
justify-content: center;
|
|
577
|
+
z-index: 1000000;
|
|
578
|
+
}
|
|
579
|
+
.hs-write-modal.open {
|
|
580
|
+
display: flex;
|
|
581
|
+
}
|
|
582
|
+
.hs-write-container {
|
|
583
|
+
background: white;
|
|
584
|
+
border-radius: 12px;
|
|
585
|
+
padding: 24px;
|
|
586
|
+
width: 90%;
|
|
587
|
+
max-width: 500px;
|
|
588
|
+
}
|
|
589
|
+
.hs-write-container h3 {
|
|
590
|
+
margin: 0 0 20px;
|
|
591
|
+
color: #333;
|
|
592
|
+
}
|
|
593
|
+
.hs-form-group {
|
|
594
|
+
margin-bottom: 16px;
|
|
595
|
+
}
|
|
596
|
+
.hs-form-group label {
|
|
597
|
+
display: block;
|
|
598
|
+
font-size: 13px;
|
|
599
|
+
color: #666;
|
|
600
|
+
margin-bottom: 6px;
|
|
601
|
+
}
|
|
602
|
+
.hs-form-group input,
|
|
603
|
+
.hs-form-group textarea {
|
|
604
|
+
width: 100%;
|
|
605
|
+
padding: 10px 12px;
|
|
606
|
+
border: 1px solid #ddd;
|
|
607
|
+
border-radius: 6px;
|
|
608
|
+
font-size: 14px;
|
|
609
|
+
box-sizing: border-box;
|
|
610
|
+
}
|
|
611
|
+
.hs-form-group textarea {
|
|
612
|
+
min-height: 120px;
|
|
613
|
+
font-family: monospace;
|
|
614
|
+
resize: vertical;
|
|
615
|
+
}
|
|
616
|
+
.hs-form-group input:focus,
|
|
617
|
+
.hs-form-group textarea:focus {
|
|
618
|
+
outline: none;
|
|
619
|
+
border-color: #667eea;
|
|
620
|
+
}
|
|
621
|
+
.hs-form-actions {
|
|
622
|
+
display: flex;
|
|
623
|
+
gap: 12px;
|
|
624
|
+
margin-top: 20px;
|
|
625
|
+
}
|
|
626
|
+
.hs-form-actions button {
|
|
627
|
+
flex: 1;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/* Responsive */
|
|
631
|
+
@media (max-width: 900px) {
|
|
632
|
+
.hs-widget-body {
|
|
633
|
+
flex-direction: column;
|
|
634
|
+
}
|
|
635
|
+
.hs-explorer-panel {
|
|
636
|
+
width: 100%;
|
|
637
|
+
height: 50%;
|
|
638
|
+
}
|
|
639
|
+
.hs-map-panel {
|
|
640
|
+
border-right: none;
|
|
641
|
+
border-bottom: 1px solid #e0e0e0;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
`;
|
|
645
|
+
|
|
646
|
+
// Initialize widget
|
|
647
|
+
function init(options = {}) {
|
|
648
|
+
config = Object.assign({
|
|
649
|
+
appName: 'holosphere-widget',
|
|
650
|
+
relays: [],
|
|
651
|
+
position: 'bottom-right',
|
|
652
|
+
theme: 'default',
|
|
653
|
+
defaultLocation: [37.7749, -122.4194],
|
|
654
|
+
defaultResolution: 9
|
|
655
|
+
}, options);
|
|
656
|
+
|
|
657
|
+
currentResolution = config.defaultResolution;
|
|
658
|
+
|
|
659
|
+
// Inject styles
|
|
660
|
+
const style = document.createElement('style');
|
|
661
|
+
style.textContent = widgetCSS;
|
|
662
|
+
document.head.appendChild(style);
|
|
663
|
+
|
|
664
|
+
// Inject Leaflet CSS if not present
|
|
665
|
+
if (!document.querySelector('link[href*="leaflet"]')) {
|
|
666
|
+
const leafletCSS = document.createElement('link');
|
|
667
|
+
leafletCSS.rel = 'stylesheet';
|
|
668
|
+
leafletCSS.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';
|
|
669
|
+
document.head.appendChild(leafletCSS);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Inject Leaflet JS if not present
|
|
673
|
+
if (!window.L) {
|
|
674
|
+
const leafletJS = document.createElement('script');
|
|
675
|
+
leafletJS.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js';
|
|
676
|
+
leafletJS.onload = () => setupWidget();
|
|
677
|
+
document.body.appendChild(leafletJS);
|
|
678
|
+
} else {
|
|
679
|
+
setupWidget();
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function setupWidget() {
|
|
684
|
+
// Inject HTML
|
|
685
|
+
const container = document.createElement('div');
|
|
686
|
+
container.id = 'hs-widget-root';
|
|
687
|
+
container.innerHTML = widgetHTML;
|
|
688
|
+
document.body.appendChild(container);
|
|
689
|
+
|
|
690
|
+
// Initialize HoloSphere
|
|
691
|
+
if (typeof HoloSphere === 'undefined') {
|
|
692
|
+
console.error('HoloSphere not found. Include holosphere.min.js before the widget.');
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
hs = new HoloSphere({
|
|
697
|
+
appName: config.appName,
|
|
698
|
+
relays: config.relays
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
// Store globally for debugging
|
|
702
|
+
window.hsWidget = { hs, map, config };
|
|
703
|
+
|
|
704
|
+
// Setup event listeners
|
|
705
|
+
setupEventListeners();
|
|
706
|
+
|
|
707
|
+
// Initialize map
|
|
708
|
+
initMap();
|
|
709
|
+
|
|
710
|
+
// Update status
|
|
711
|
+
updateStatus('connected', 'Ready');
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function setupEventListeners() {
|
|
715
|
+
// Toggle button
|
|
716
|
+
document.getElementById('hs-widget-button').addEventListener('click', toggleModal);
|
|
717
|
+
|
|
718
|
+
// Close button
|
|
719
|
+
document.getElementById('hs-close-btn').addEventListener('click', closeModal);
|
|
720
|
+
|
|
721
|
+
// Close on backdrop click
|
|
722
|
+
document.getElementById('hs-widget-modal').addEventListener('click', (e) => {
|
|
723
|
+
if (e.target.id === 'hs-widget-modal') closeModal();
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
// Resolution slider
|
|
727
|
+
document.getElementById('hs-resolution-slider').addEventListener('input', (e) => {
|
|
728
|
+
currentResolution = parseInt(e.target.value);
|
|
729
|
+
document.getElementById('hs-resolution-value').textContent = currentResolution;
|
|
730
|
+
if (currentHolon) {
|
|
731
|
+
const center = map.getCenter();
|
|
732
|
+
selectHolonAt(center.lat, center.lng);
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
// Tabs
|
|
737
|
+
document.querySelectorAll('.hs-tab').forEach(tab => {
|
|
738
|
+
tab.addEventListener('click', () => {
|
|
739
|
+
document.querySelectorAll('.hs-tab').forEach(t => t.classList.remove('active'));
|
|
740
|
+
document.querySelectorAll('.hs-tab-content').forEach(c => c.classList.remove('active'));
|
|
741
|
+
tab.classList.add('active');
|
|
742
|
+
document.getElementById(`hs-tab-${tab.dataset.tab}`).classList.add('active');
|
|
743
|
+
});
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
// Lens selector
|
|
747
|
+
document.getElementById('hs-lens-select').addEventListener('change', loadLensData);
|
|
748
|
+
|
|
749
|
+
// Add lens button
|
|
750
|
+
document.getElementById('hs-add-lens-btn').addEventListener('click', () => {
|
|
751
|
+
const lens = prompt('Enter new lens name:');
|
|
752
|
+
if (lens) {
|
|
753
|
+
addLensOption(lens);
|
|
754
|
+
document.getElementById('hs-lens-select').value = lens;
|
|
755
|
+
loadLensData();
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
// Write button
|
|
760
|
+
document.getElementById('hs-write-btn').addEventListener('click', openWriteModal);
|
|
761
|
+
document.getElementById('hs-write-cancel').addEventListener('click', closeWriteModal);
|
|
762
|
+
document.getElementById('hs-write-submit').addEventListener('click', submitWriteData);
|
|
763
|
+
|
|
764
|
+
// Keyboard shortcuts
|
|
765
|
+
document.addEventListener('keydown', (e) => {
|
|
766
|
+
if (e.key === 'Escape' && isOpen) closeModal();
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function initMap() {
|
|
771
|
+
map = L.map('hs-map').setView(config.defaultLocation, resolutionToZoom[currentResolution]);
|
|
772
|
+
|
|
773
|
+
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
774
|
+
attribution: '© OpenStreetMap contributors'
|
|
775
|
+
}).addTo(map);
|
|
776
|
+
|
|
777
|
+
// Click handler
|
|
778
|
+
map.on('click', (e) => {
|
|
779
|
+
selectHolonAt(e.latlng.lat, e.latlng.lng);
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
// Zoom handler
|
|
783
|
+
map.on('zoomend', () => {
|
|
784
|
+
const zoom = map.getZoom();
|
|
785
|
+
const newRes = getResolutionForZoom(zoom);
|
|
786
|
+
if (newRes !== currentResolution) {
|
|
787
|
+
currentResolution = newRes;
|
|
788
|
+
document.getElementById('hs-resolution-slider').value = newRes;
|
|
789
|
+
document.getElementById('hs-resolution-value').textContent = newRes;
|
|
790
|
+
if (currentHolon) {
|
|
791
|
+
const center = map.getCenter();
|
|
792
|
+
selectHolonAt(center.lat, center.lng);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
// Mousemove for coordinates
|
|
798
|
+
map.on('mousemove', (e) => {
|
|
799
|
+
const lat = e.latlng.lat.toFixed(6);
|
|
800
|
+
const lng = e.latlng.lng.toFixed(6);
|
|
801
|
+
document.getElementById('hs-coords').innerHTML =
|
|
802
|
+
`Lat: ${lat}, Lng: ${lng} | Resolution: ${currentResolution}`;
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function getResolutionForZoom(zoom) {
|
|
807
|
+
// Find closest resolution for zoom level
|
|
808
|
+
let closest = 9;
|
|
809
|
+
let minDiff = Infinity;
|
|
810
|
+
for (const [res, z] of Object.entries(resolutionToZoom)) {
|
|
811
|
+
const diff = Math.abs(z - zoom);
|
|
812
|
+
if (diff < minDiff) {
|
|
813
|
+
minDiff = diff;
|
|
814
|
+
closest = parseInt(res);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
return Math.min(15, Math.max(0, closest));
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
async function selectHolonAt(lat, lng) {
|
|
821
|
+
try {
|
|
822
|
+
const holon = await hs.toHolon(lat, lng, currentResolution);
|
|
823
|
+
currentHolon = holon;
|
|
824
|
+
|
|
825
|
+
// Update UI
|
|
826
|
+
document.getElementById('hs-current-holon').textContent = holon;
|
|
827
|
+
document.getElementById('hs-write-btn').disabled = false;
|
|
828
|
+
|
|
829
|
+
// Draw hexagon on map
|
|
830
|
+
await drawHexagon(holon);
|
|
831
|
+
|
|
832
|
+
// Load hierarchy
|
|
833
|
+
await loadHierarchy(holon);
|
|
834
|
+
|
|
835
|
+
// Load available lenses
|
|
836
|
+
await loadLenses(holon);
|
|
837
|
+
|
|
838
|
+
// Update coordinates display
|
|
839
|
+
document.getElementById('hs-coords').innerHTML =
|
|
840
|
+
`<strong>Holon:</strong> ${holon.substring(0, 12)}... | Resolution: ${currentResolution}`;
|
|
841
|
+
|
|
842
|
+
} catch (error) {
|
|
843
|
+
console.error('Error selecting holon:', error);
|
|
844
|
+
updateStatus('error', error.message);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
async function drawHexagon(holon) {
|
|
849
|
+
// Remove previous selection
|
|
850
|
+
if (selectedHexLayer) {
|
|
851
|
+
map.removeLayer(selectedHexLayer);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// Get hexagon boundary using h3-js
|
|
855
|
+
// The HoloSphere CDN bundle exposes h3 as window.h3
|
|
856
|
+
try {
|
|
857
|
+
const h3 = window.h3;
|
|
858
|
+
|
|
859
|
+
if (h3 && h3.cellToBoundary) {
|
|
860
|
+
const boundary = h3.cellToBoundary(holon);
|
|
861
|
+
const latLngs = boundary.map(([lat, lng]) => [lat, lng]);
|
|
862
|
+
|
|
863
|
+
selectedHexLayer = L.polygon(latLngs, {
|
|
864
|
+
color: colors.strokeSelected,
|
|
865
|
+
weight: 3,
|
|
866
|
+
fillColor: colors.selected,
|
|
867
|
+
fillOpacity: 0.5
|
|
868
|
+
}).addTo(map);
|
|
869
|
+
|
|
870
|
+
// Add popup with holon info
|
|
871
|
+
selectedHexLayer.bindPopup(`
|
|
872
|
+
<strong>Holon:</strong> ${holon}<br>
|
|
873
|
+
<strong>Resolution:</strong> ${h3.getResolution(holon)}<br>
|
|
874
|
+
<strong>Area:</strong> ${(h3.cellArea(holon, 'km2')).toFixed(2)} km²
|
|
875
|
+
`);
|
|
876
|
+
|
|
877
|
+
// Fit map to hexagon with animation
|
|
878
|
+
map.fitBounds(selectedHexLayer.getBounds(), {
|
|
879
|
+
padding: [50, 50],
|
|
880
|
+
animate: true,
|
|
881
|
+
duration: 0.5
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
// Also draw neighboring hexagons (ring 1) for context
|
|
885
|
+
drawNeighborHexagons(holon, 1);
|
|
886
|
+
} else {
|
|
887
|
+
// Fallback: draw a circle at the location
|
|
888
|
+
const center = map.getCenter();
|
|
889
|
+
selectedHexLayer = L.circleMarker([center.lat, center.lng], {
|
|
890
|
+
radius: 20,
|
|
891
|
+
color: colors.strokeSelected,
|
|
892
|
+
fillColor: colors.selected,
|
|
893
|
+
fillOpacity: 0.5
|
|
894
|
+
}).addTo(map);
|
|
895
|
+
}
|
|
896
|
+
} catch (e) {
|
|
897
|
+
console.warn('Could not draw hexagon boundary:', e);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function drawNeighborHexagons(centerHolon, ring) {
|
|
902
|
+
// Clear previous neighbor layers
|
|
903
|
+
Object.values(hexagonLayers).forEach(layer => {
|
|
904
|
+
if (layer !== selectedHexLayer) map.removeLayer(layer);
|
|
905
|
+
});
|
|
906
|
+
hexagonLayers = {};
|
|
907
|
+
|
|
908
|
+
const h3 = window.h3;
|
|
909
|
+
if (!h3 || !h3.gridDisk) return;
|
|
910
|
+
|
|
911
|
+
try {
|
|
912
|
+
const neighbors = h3.gridDisk(centerHolon, ring);
|
|
913
|
+
|
|
914
|
+
neighbors.forEach(neighbor => {
|
|
915
|
+
if (neighbor === centerHolon) return; // Skip center
|
|
916
|
+
|
|
917
|
+
const boundary = h3.cellToBoundary(neighbor);
|
|
918
|
+
const latLngs = boundary.map(([lat, lng]) => [lat, lng]);
|
|
919
|
+
|
|
920
|
+
const layer = L.polygon(latLngs, {
|
|
921
|
+
color: colors.stroke,
|
|
922
|
+
weight: 1,
|
|
923
|
+
fillColor: colors.empty,
|
|
924
|
+
fillOpacity: 0.3,
|
|
925
|
+
className: 'hs-neighbor-hex'
|
|
926
|
+
}).addTo(map);
|
|
927
|
+
|
|
928
|
+
// Make neighbors clickable
|
|
929
|
+
layer.on('click', () => {
|
|
930
|
+
selectHolonAt(...h3.cellToLatLng(neighbor));
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
layer.on('mouseover', () => {
|
|
934
|
+
layer.setStyle({ fillColor: colors.hover, fillOpacity: 0.5 });
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
layer.on('mouseout', () => {
|
|
938
|
+
layer.setStyle({ fillColor: colors.empty, fillOpacity: 0.3 });
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
hexagonLayers[neighbor] = layer;
|
|
942
|
+
});
|
|
943
|
+
} catch (e) {
|
|
944
|
+
console.warn('Could not draw neighbor hexagons:', e);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
async function loadHierarchy(holon) {
|
|
949
|
+
const container = document.getElementById('hs-hierarchy');
|
|
950
|
+
container.innerHTML = '<div class="hs-empty-state">Loading hierarchy...</div>';
|
|
951
|
+
|
|
952
|
+
try {
|
|
953
|
+
// Get parents
|
|
954
|
+
const parents = await hs.getParents(holon, 0);
|
|
955
|
+
|
|
956
|
+
// Get children (only if resolution allows)
|
|
957
|
+
let children = [];
|
|
958
|
+
const res = await getHolonResolution(holon);
|
|
959
|
+
if (res < 15) {
|
|
960
|
+
children = await hs.getChildren(holon);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
let html = '';
|
|
964
|
+
|
|
965
|
+
// Parents section
|
|
966
|
+
if (parents && parents.length > 0) {
|
|
967
|
+
html += '<div class="hs-hierarchy-section">';
|
|
968
|
+
html += '<div class="hs-hierarchy-section-title">Parent Holons (Larger Areas)</div>';
|
|
969
|
+
for (const parent of parents.reverse()) {
|
|
970
|
+
const pRes = await getHolonResolution(parent);
|
|
971
|
+
html += `
|
|
972
|
+
<div class="hs-hierarchy-item" data-holon="${parent}">
|
|
973
|
+
<span class="resolution">${pRes}</span>
|
|
974
|
+
<span class="holon-id">${parent}</span>
|
|
975
|
+
</div>
|
|
976
|
+
`;
|
|
977
|
+
}
|
|
978
|
+
html += '</div>';
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// Current holon
|
|
982
|
+
html += '<div class="hs-hierarchy-section">';
|
|
983
|
+
html += '<div class="hs-hierarchy-section-title">Current Holon</div>';
|
|
984
|
+
html += `
|
|
985
|
+
<div class="hs-hierarchy-item current">
|
|
986
|
+
<span class="resolution">${res}</span>
|
|
987
|
+
<span class="holon-id">${holon}</span>
|
|
988
|
+
</div>
|
|
989
|
+
`;
|
|
990
|
+
html += '</div>';
|
|
991
|
+
|
|
992
|
+
// Children section
|
|
993
|
+
if (children && children.length > 0) {
|
|
994
|
+
html += '<div class="hs-hierarchy-section">';
|
|
995
|
+
html += '<div class="hs-hierarchy-section-title">Child Holons (Smaller Areas)</div>';
|
|
996
|
+
const childRes = res + 1;
|
|
997
|
+
for (const child of children.slice(0, 7)) { // Show first 7
|
|
998
|
+
html += `
|
|
999
|
+
<div class="hs-hierarchy-item" data-holon="${child}">
|
|
1000
|
+
<span class="resolution">${childRes}</span>
|
|
1001
|
+
<span class="holon-id">${child}</span>
|
|
1002
|
+
</div>
|
|
1003
|
+
`;
|
|
1004
|
+
}
|
|
1005
|
+
if (children.length > 7) {
|
|
1006
|
+
html += `<div class="hs-empty-state">+${children.length - 7} more children</div>`;
|
|
1007
|
+
}
|
|
1008
|
+
html += '</div>';
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
container.innerHTML = html;
|
|
1012
|
+
|
|
1013
|
+
// Add click handlers
|
|
1014
|
+
container.querySelectorAll('.hs-hierarchy-item:not(.current)').forEach(item => {
|
|
1015
|
+
item.addEventListener('click', async () => {
|
|
1016
|
+
const targetHolon = item.dataset.holon;
|
|
1017
|
+
const targetRes = await getHolonResolution(targetHolon);
|
|
1018
|
+
currentResolution = targetRes;
|
|
1019
|
+
document.getElementById('hs-resolution-slider').value = targetRes;
|
|
1020
|
+
document.getElementById('hs-resolution-value').textContent = targetRes;
|
|
1021
|
+
|
|
1022
|
+
currentHolon = targetHolon;
|
|
1023
|
+
document.getElementById('hs-current-holon').textContent = targetHolon;
|
|
1024
|
+
|
|
1025
|
+
await drawHexagon(targetHolon);
|
|
1026
|
+
await loadHierarchy(targetHolon);
|
|
1027
|
+
await loadLenses(targetHolon);
|
|
1028
|
+
});
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
} catch (error) {
|
|
1032
|
+
container.innerHTML = `<div class="hs-empty-state">Error: ${error.message}</div>`;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
async function getHolonResolution(holon) {
|
|
1037
|
+
// Get resolution from h3-js (exposed as window.h3 by the CDN bundle)
|
|
1038
|
+
const h3 = window.h3;
|
|
1039
|
+
if (h3 && h3.getResolution) {
|
|
1040
|
+
return h3.getResolution(holon);
|
|
1041
|
+
}
|
|
1042
|
+
// Fallback: estimate from holon ID length (not accurate but works)
|
|
1043
|
+
return Math.min(15, Math.max(0, holon.length - 8));
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
async function loadLenses(holon) {
|
|
1047
|
+
const container = document.getElementById('hs-lenses-list');
|
|
1048
|
+
const select = document.getElementById('hs-lens-select');
|
|
1049
|
+
|
|
1050
|
+
// Default lenses
|
|
1051
|
+
const defaultLenses = ['notes', 'events', 'places', 'resources', 'projects'];
|
|
1052
|
+
|
|
1053
|
+
// Clear and populate select
|
|
1054
|
+
select.innerHTML = '<option value="">Select lens...</option>';
|
|
1055
|
+
defaultLenses.forEach(lens => addLensOption(lens));
|
|
1056
|
+
|
|
1057
|
+
// Update lenses list
|
|
1058
|
+
let html = '';
|
|
1059
|
+
for (const lens of defaultLenses) {
|
|
1060
|
+
try {
|
|
1061
|
+
const data = await hs.getAll(holon, lens);
|
|
1062
|
+
const count = data ? data.length : 0;
|
|
1063
|
+
html += `
|
|
1064
|
+
<div class="hs-lens-item" data-lens="${lens}">
|
|
1065
|
+
<span class="hs-lens-name">${lens}</span>
|
|
1066
|
+
<span class="hs-lens-count">${count} items</span>
|
|
1067
|
+
</div>
|
|
1068
|
+
`;
|
|
1069
|
+
} catch (e) {
|
|
1070
|
+
html += `
|
|
1071
|
+
<div class="hs-lens-item" data-lens="${lens}">
|
|
1072
|
+
<span class="hs-lens-name">${lens}</span>
|
|
1073
|
+
<span class="hs-lens-count">0 items</span>
|
|
1074
|
+
</div>
|
|
1075
|
+
`;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
container.innerHTML = html || '<div class="hs-empty-state">No data in this holon</div>';
|
|
1080
|
+
|
|
1081
|
+
// Click handlers
|
|
1082
|
+
container.querySelectorAll('.hs-lens-item').forEach(item => {
|
|
1083
|
+
item.addEventListener('click', () => {
|
|
1084
|
+
select.value = item.dataset.lens;
|
|
1085
|
+
loadLensData();
|
|
1086
|
+
document.querySelector('.hs-tab[data-tab="data"]').click();
|
|
1087
|
+
});
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function addLensOption(lens) {
|
|
1092
|
+
const select = document.getElementById('hs-lens-select');
|
|
1093
|
+
if (!select.querySelector(`option[value="${lens}"]`)) {
|
|
1094
|
+
const option = document.createElement('option');
|
|
1095
|
+
option.value = lens;
|
|
1096
|
+
option.textContent = lens;
|
|
1097
|
+
select.appendChild(option);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
async function loadLensData() {
|
|
1102
|
+
const lens = document.getElementById('hs-lens-select').value;
|
|
1103
|
+
const container = document.getElementById('hs-data-list');
|
|
1104
|
+
|
|
1105
|
+
if (!lens || !currentHolon) {
|
|
1106
|
+
container.innerHTML = '<div class="hs-empty-state">Select a lens to view data</div>';
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
container.innerHTML = '<div class="hs-empty-state">Loading...</div>';
|
|
1111
|
+
|
|
1112
|
+
try {
|
|
1113
|
+
const data = await hs.getAll(currentHolon, lens);
|
|
1114
|
+
|
|
1115
|
+
if (!data || data.length === 0) {
|
|
1116
|
+
container.innerHTML = '<div class="hs-empty-state">No data in this lens</div>';
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
let html = '';
|
|
1121
|
+
for (const item of data) {
|
|
1122
|
+
const title = item.title || item.name || item.id || 'Untitled';
|
|
1123
|
+
const preview = JSON.stringify(item, null, 2).substring(0, 150);
|
|
1124
|
+
const meta = item._meta ? new Date(item._meta.timestamp).toLocaleString() : '';
|
|
1125
|
+
|
|
1126
|
+
html += `
|
|
1127
|
+
<div class="hs-data-item" data-id="${item.id}">
|
|
1128
|
+
<div class="hs-data-item-title">${escapeHtml(title)}</div>
|
|
1129
|
+
<div class="hs-data-item-meta">${meta}</div>
|
|
1130
|
+
<div class="hs-data-item-preview"><code>${escapeHtml(preview)}...</code></div>
|
|
1131
|
+
</div>
|
|
1132
|
+
`;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
container.innerHTML = html;
|
|
1136
|
+
|
|
1137
|
+
// Click handlers
|
|
1138
|
+
container.querySelectorAll('.hs-data-item').forEach(item => {
|
|
1139
|
+
item.addEventListener('click', () => {
|
|
1140
|
+
const id = item.dataset.id;
|
|
1141
|
+
const dataItem = data.find(d => d.id === id);
|
|
1142
|
+
if (dataItem) {
|
|
1143
|
+
alert(JSON.stringify(dataItem, null, 2));
|
|
1144
|
+
}
|
|
1145
|
+
});
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
} catch (error) {
|
|
1149
|
+
container.innerHTML = `<div class="hs-empty-state">Error: ${error.message}</div>`;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
function openWriteModal() {
|
|
1154
|
+
document.getElementById('hs-write-holon').value = currentHolon;
|
|
1155
|
+
document.getElementById('hs-write-lens').value = document.getElementById('hs-lens-select').value || '';
|
|
1156
|
+
document.getElementById('hs-write-data').value = '{\n "title": "",\n "description": ""\n}';
|
|
1157
|
+
document.getElementById('hs-write-modal').classList.add('open');
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
function closeWriteModal() {
|
|
1161
|
+
document.getElementById('hs-write-modal').classList.remove('open');
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
async function submitWriteData() {
|
|
1165
|
+
const holon = document.getElementById('hs-write-holon').value;
|
|
1166
|
+
const lens = document.getElementById('hs-write-lens').value;
|
|
1167
|
+
const dataStr = document.getElementById('hs-write-data').value;
|
|
1168
|
+
|
|
1169
|
+
if (!holon || !lens) {
|
|
1170
|
+
alert('Please enter holon and lens');
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
try {
|
|
1175
|
+
const data = JSON.parse(dataStr);
|
|
1176
|
+
await hs.write(holon, lens, data);
|
|
1177
|
+
|
|
1178
|
+
closeWriteModal();
|
|
1179
|
+
addLensOption(lens);
|
|
1180
|
+
document.getElementById('hs-lens-select').value = lens;
|
|
1181
|
+
loadLensData();
|
|
1182
|
+
loadLenses(holon);
|
|
1183
|
+
|
|
1184
|
+
updateStatus('connected', 'Data written successfully');
|
|
1185
|
+
} catch (error) {
|
|
1186
|
+
alert('Error: ' + error.message);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function toggleModal() {
|
|
1191
|
+
if (isOpen) {
|
|
1192
|
+
closeModal();
|
|
1193
|
+
} else {
|
|
1194
|
+
openModal();
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
function openModal() {
|
|
1199
|
+
document.getElementById('hs-widget-modal').classList.add('open');
|
|
1200
|
+
document.getElementById('hs-widget-button').classList.add('active');
|
|
1201
|
+
isOpen = true;
|
|
1202
|
+
|
|
1203
|
+
// Trigger map resize after modal opens
|
|
1204
|
+
setTimeout(() => {
|
|
1205
|
+
if (map) map.invalidateSize();
|
|
1206
|
+
}, 300);
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
function closeModal() {
|
|
1210
|
+
document.getElementById('hs-widget-modal').classList.remove('open');
|
|
1211
|
+
document.getElementById('hs-widget-button').classList.remove('active');
|
|
1212
|
+
isOpen = false;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
function updateStatus(type, message) {
|
|
1216
|
+
const dot = document.querySelector('.hs-status-dot');
|
|
1217
|
+
const text = document.getElementById('hs-status-text');
|
|
1218
|
+
|
|
1219
|
+
dot.classList.remove('connected', 'error');
|
|
1220
|
+
if (type === 'connected') dot.classList.add('connected');
|
|
1221
|
+
text.textContent = message;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
function escapeHtml(str) {
|
|
1225
|
+
const div = document.createElement('div');
|
|
1226
|
+
div.textContent = str;
|
|
1227
|
+
return div.innerHTML;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// Public API
|
|
1231
|
+
global.HoloSphereWidget = {
|
|
1232
|
+
init,
|
|
1233
|
+
open: openModal,
|
|
1234
|
+
close: closeModal,
|
|
1235
|
+
toggle: toggleModal,
|
|
1236
|
+
selectHolon: selectHolonAt,
|
|
1237
|
+
getHoloSphere: () => hs,
|
|
1238
|
+
getMap: () => map,
|
|
1239
|
+
getCurrentHolon: () => currentHolon
|
|
1240
|
+
};
|
|
1241
|
+
|
|
1242
|
+
})(typeof window !== 'undefined' ? window : this);
|