holosphere 1.0.4 → 1.0.6

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.
@@ -0,0 +1,22 @@
1
+ declare module 'holosphere' {
2
+ export default class HoloSphere {
3
+ constructor(name: string, openaikey?: string | null);
4
+ subscribe(hex: string, lense: string, callback: (item: any, key: string) => void): void;
5
+ put(hex: string, lense: string, content: any): Promise<void>;
6
+ delete(id: string, tag: string): Promise<void>;
7
+ get(hex: string, lense: string): Promise<any[]>;
8
+ getKey(hex: string, lense: string, key: string): Promise<any | null>;
9
+ getNode(hex: string, lense: string, key: string): Promise<any | null>;
10
+ compute(hex: string, lense: string, operation: string): Promise<void>;
11
+ clearlense(hex: string, lense: string): Promise<void>;
12
+ summarize(history: string): Promise<string>;
13
+ upcast(hex: string, lense: string, content: any): Promise<any>;
14
+ updateParent(id: string, report: string): Promise<any>;
15
+ getHex(lat: number, lng: number, resolution: number): Promise<string>;
16
+ getScalespace(lat: number, lng: number): string[];
17
+ getHexScalespace(hex: string): string[];
18
+ aggregateVotes(hexId: string, topic: string): object;
19
+ delegateVote(userId: string, topic: string, delegateTo: string): Promise<void>;
20
+ vote(userId: string, hexId: string, topic: string, vote: string): Promise<void>;
21
+ }
22
+ }
@@ -4,10 +4,15 @@ import Gun from 'gun'
4
4
  import Ajv2019 from 'ajv/dist/2019.js'
5
5
 
6
6
  class HoloSphere {
7
+ /**
8
+ * Initializes a new instance of the HoloSphere class.
9
+ * @param {string} appname - The name of the application.
10
+ * @param {string|null} openaikey - The OpenAI API key.
11
+ */
7
12
  constructor(appname, openaikey = null) {
8
13
  this.validator = new Ajv2019({ allErrors: false, strict: false });
9
14
  this.gun = Gun({
10
- peers: ['http://gun.holons.io', 'https://59.src.eco/gun'],
15
+ peers: ['https://59.src.eco/gun'],
11
16
  axe: false,
12
17
  // uuid: (content) => { // generate a unique id for each node
13
18
  // console.log('uuid', content);
@@ -15,119 +20,22 @@ class HoloSphere {
15
20
  });
16
21
 
17
22
  this.gun = this.gun.get(appname)
23
+ this.users = {}; // Initialize users
24
+ this.hexagonVotes = {}; // Initialize hexagonVotes
18
25
 
19
26
  if (openaikey != null) {
20
27
  this.openai = new OpenAI({
21
28
  apiKey: openaikey,
22
29
  });
23
30
  }
24
-
25
- //this.bot.command('sethex', async (ctx) => { this.setHex(ctx) }) TODO: MOVE HERE FROM SETTINGS
26
-
27
- // this.bot.command('resethex', async (ctx) => {
28
- // let chatID = ctx.message.chat.id;
29
- // let hex = (await this.db.get('settings', chatID)).hex
30
- // this.delete(hex, ctx.message.text.split(' ')[1])
31
- // })
32
-
33
- // this.bot.command('get', async (ctx) => {
34
- // const chatID = ctx.message.chat.id;
35
- // const lense = ctx.message.text.split(' ')[1];
36
- // if (!lense) {
37
- // return ctx.reply('Please specify a tag.');
38
- // }
39
- // let hex = (await this.db.get('settings', chatID)).hex
40
- // //let hex = settings.hex
41
- // console.log('hex', hex)
42
-
43
- // let data = await this.get(ctx, hex, lense)
44
-
45
- // })
46
-
47
- // this.bot.command('gethex', async (ctx) => {
48
- // let settings = await this.db.get('settings', chatID)
49
- // let id = settings.hex ? settings.hex : 'Hex not set, use /sethex'
50
- // ctx.reply(id)
51
- // })
52
-
53
- // this.bot.command('compute', async (ctx) => {
54
- // const chatID = ctx.message.chat.id;
55
- // let operation = ctx.message.text.split(' ')[1];
56
- // if (operation != 'sum') {
57
- // ctx.reply('Operation not implemented')
58
- // return
59
- // }
60
- // let lense = ctx.message.text.split(' ')[2]
61
- // if (!lense) {
62
- // ctx.reply('Please specify a lense where to perform the operation ')
63
- // return
64
- // }
65
- // let hex = (await this.db.get('settings', chatID)).hex
66
- // await this.compute(hex, lense, operation)
67
-
68
- // // let parentInfo = await this.getCellInfo(parent)
69
- // // parentInfo.wisdom[id] = report
70
- // // //update summary
71
- // // let summary = await this.summarize(Object.values(parentInfo.wisdom).join('\n'))
72
- // // parentInfo.summary = summary
73
-
74
- // // await this.db.put('cell', parentInfo)
75
- // // return parentInfo
76
-
77
- // // let content = await this.db.getAll(hex+'/tags')
78
- // })
79
-
80
-
81
-
82
-
83
- // this.bot.command('cast', async (ctx) => {
84
- // if (!ctx.message.reply_to_message) {
85
- // return ctx.reply('Please reply to a message you want to tag.');
86
- // }
87
- // const tags = ctx.message.text.split(' ').slice(1);
88
- // if (tags.length === 0) {
89
- // return ctx.reply('Please provide at least one tag.');
90
- // }
91
-
92
- // const messageID = ctx.message.reply_to_message.message_id;
93
- // const chatID = ctx.message.chat.id;
94
- // const messageContent = ctx.message.reply_to_message.text;
95
- // let settings = await this.db.get('settings', chatID)
96
- // let id = settings.hex ? settings.hex : 'Hex not set, use /sethex'
97
- // //create root node for the item
98
- // let node = await this.gun.get(chatID + '/' + messageID).put({ id: chatID + '/' + messageID, content: messageContent })
99
- // for (let tag of tags) {
100
- // await this.gun.get(id).get(tag).set(node)
101
- // this.upcast(id, tag, node)
102
- // }
103
- // })
104
-
105
- // this.bot.command('publish', async (ctx) => {
106
- // console.log(ctx.message)
107
- // if (!ctx.message.reply_to_message) {
108
- // return ctx.reply('Please reply to a message you want to tag.');
109
- // }
110
- // const tags = ctx.message.text.split(' ').slice(1);
111
- // if (tags.length === 0) {
112
- // return ctx.reply('Please provide at least one tag.');
113
- // }
114
-
115
- // const messageID = ctx.message.reply_to_message.message_id;
116
- // const chatID = ctx.message.chat.id;
117
- // const messageContent = ctx.message.reply_to_message.text;
118
- // let settings = await this.db.get('settings', chatID)
119
- // let hex = settings.hex
120
-
121
- // for (let tag of tags) {
122
- // let node = await this.gun.get(chatID + '/' + messageID).put({ id: chatID + '/' + messageID, content: messageContent })
123
- // await this.put(hex, tag, node)
124
- // }
125
-
126
- // ctx.reply('Tag published.');
127
- // });
128
-
129
31
  }
130
32
 
33
+ /**
34
+ * Sets the JSON schema for a specific lense.
35
+ * @param {string} lense - The lense identifier.
36
+ * @param {object} schema - The JSON schema to set.
37
+ * @returns {Promise} - Resolves when the schema is set.
38
+ */
131
39
  async setSchema(lense, schema) {
132
40
  return new Promise((resolve, reject) => {
133
41
  this.gun.get(lense).get('schema').put(JSON.stringify(schema), ack => {
@@ -141,6 +49,11 @@ class HoloSphere {
141
49
  })
142
50
  }
143
51
 
52
+ /**
53
+ * Retrieves the JSON schema for a specific lense.
54
+ * @param {string} lense - The lense identifier.
55
+ * @returns {Promise<object|null>} - The retrieved schema or null if not found.
56
+ */
144
57
  async getSchema(lense) {
145
58
  return new Promise((resolve) => {
146
59
  this.gun.get(lense).get('schema').once(data => {
@@ -159,28 +72,21 @@ class HoloSphere {
159
72
  })
160
73
  })
161
74
  }
162
-
163
- async setHex(ctx) {
164
- const chatID = ctx.message.chat.id;
165
- const hex = ctx.message.text.split(' ')[1];
166
- this.db.gun.get(hex).set('chats').put(chatID)
167
- this.db.gun.get('settings').get(chatID).put(hex)
168
- return hex
169
- }
170
-
171
- async getHexContent(ctx) {
172
- const chatID = ctx.message.chat.id;
173
- let settings = await this.getSettings(chatID)
174
- let hex = settings.hex
175
- let content = await this.db.getAll(hex + '/tags')
176
- //console.log(content)
177
- return content ? content[0].id : 'not found'
178
- }
179
-
75
+ /**
76
+ * Deletes a specific tag from a given ID.
77
+ * @param {string} id - The identifier from which to delete the tag.
78
+ * @param {string} tag - The tag to delete.
79
+ */
180
80
  async delete(id, tag) {
181
81
  await this.gun.get(id).get(tag).put(null)
182
82
  }
183
83
 
84
+ /**
85
+ * Stores content in the specified hex and lense.
86
+ * @param {string} hex - The hex identifier.
87
+ * @param {string} lense - The lense under which to store the content.
88
+ * @param {object} content - The content to store.
89
+ */
184
90
  async put(hex, lense, content) {
185
91
  // Retrieve the schema for the lense
186
92
  let schema = await this.getSchema(lense)
@@ -202,28 +108,47 @@ class HoloSphere {
202
108
  noderef = this.gun.get(lense).get(content.id).put(payload)
203
109
  this.gun.get(hex.toString()).get(lense).get(content.id).put(payload)
204
110
  } else { // create a content-addressable reference like IPFS. Note: no updates possible using put
205
- //const hash = createHash("sha256").update(payload).digest("hex");
206
- const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(payload));
207
- // const hashHex = Array.from(new Uint8Array(hash))
208
- // .map(byte => byte.toString(16).padStart(2, "0"))
209
- // .join("");
210
- noderef = this.gun.get(lense).get(hash).put(payload)
211
- this.gun.get(hex.toString()).get(lense).get(hash).put(payload)
111
+ const hashBuffer = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(payload));
112
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
113
+ const hashHex = hashArray.map(byte => byte.toString(16).padStart(2, "0")).join("");
114
+ noderef = this.gun.get(lense).get(hashHex).put(payload)
115
+ this.gun.get(hex.toString()).get(lense).get(hashHex).put(payload)
212
116
  }
213
117
 
214
- // return new Promise((resolve, reject) => {
215
- // this.gun.get(hex.toString()).get(lense).set(noderef, ack => {
216
- // if (ack.err) {
217
- // reject(new Error('Failed to add content: ' + ack.err));
218
- // } else {
219
- // console.log('Content added successfully under tag:', lense);
220
- // resolve(ack);
221
- // }
222
- // });
223
- // });
118
+ }
119
+
120
+ async putNode(hex, lense, node) {
121
+ this.gun.get(hex).get(lense).set(node)
122
+ }
224
123
 
124
+ async parse(data) {
125
+ var parsed = {}
126
+
127
+ if (data._ && data._["#"]) {
128
+ // If the data is a reference, fetch the actual content
129
+ let query = data._['#'].split('/')
130
+ let hex = query[1]
131
+ let lense = query[2]
132
+ let key = query[3]
133
+ parsed = await this.getKey(hex, lense, key)
134
+
135
+ } else
136
+
137
+ try {
138
+ parsed = JSON.parse(data);
139
+ } catch (e) {
140
+ console.log('Invalid JSON:', data);
141
+ parsed = data //return the raw data
142
+ }
143
+ return parsed
225
144
  }
226
145
 
146
+ /**
147
+ * Retrieves content from the specified hex and lense.
148
+ * @param {string} hex - The hex identifier.
149
+ * @param {string} lense - The lense from which to retrieve content.
150
+ * @returns {Promise<Array<object>>} - The retrieved content.
151
+ */
227
152
  async get(hex, lense) {
228
153
  // Wrap the GunDB operation in a promise
229
154
  //retrieve lense schema
@@ -244,18 +169,8 @@ class HoloSphere {
244
169
  this.gun.get(hex.toString()).get(lense).map().once(async (itemdata, key) => {
245
170
  counter += 1
246
171
  if (itemdata) {
247
- // if (itemdata._["#"]) {
248
- // // If the data is a reference, fetch the actual content
249
- // itemdata = await this.gun.get(itemdata._['#']).then();
250
- // console.log("Data :",itemdata)
251
- // }
252
- var parsed = {}
253
- try {
254
- parsed = JSON.parse(itemdata);
255
- } catch (e) {
256
- console.log('Invalid JSON:', itemdata);
257
- parsed = itemdata //return the raw data
258
- }
172
+ let parsed = await this.parse (itemdata)
173
+
259
174
 
260
175
  if (schema) {
261
176
  let valid = this.validator.validate(schema, parsed);
@@ -283,8 +198,47 @@ class HoloSphere {
283
198
  );
284
199
  }
285
200
 
286
- // Operations
201
+ /**
202
+ * Retrieves a specific key from the specified hex and lense.
203
+ * @param {string} hex - The hex identifier.
204
+ * @param {string} lense - The lense from which to retrieve the key.
205
+ * @param {string} key - The specific key to retrieve.
206
+ * @returns {Promise<object|null>} - The retrieved content or null if not found.
207
+ */
208
+ async getKey(hex, lense, key) {
209
+ return new Promise((resolve) => {
210
+ // Use Gun to get the data
211
+ this.gun.get(hex).get(lense).get(key).once((data, key) => {
212
+ if (data) {
213
+ resolve(JSON.parse(data)); // Resolve the promise with the data if data is found
214
+ } else {
215
+ resolve(null); // Reject the promise if no data is found
216
+ }
217
+ });
218
+ });
219
+
220
+ }
221
+
222
+ /**
223
+ * Retrieves a specific gundb node from the specified hex and lense.
224
+ * @param {string} hex - The hex identifier.
225
+ * @param {string} lense - The lense from which to retrieve the key.
226
+ * @param {string} key - The specific key to retrieve.
227
+ * @returns {Promise<object|null>} - The retrieved content or null if not found.
228
+ */
229
+ getNode(hex, lense, key) {
230
+ // Use Gun to get the data
231
+ return this.gun.get(hex).get(lense).get(key)
232
+ }
233
+
287
234
 
235
+
236
+ /**
237
+ * Computes summaries based on the content within a hex and lense.
238
+ * @param {string} hex - The hex identifier.
239
+ * @param {string} lense - The lense to compute.
240
+ * @param {string} operation - The operation to perform.
241
+ */
288
242
  async compute(hex, lense, operation) {
289
243
 
290
244
  let res = h3.getResolution(hex);
@@ -304,7 +258,7 @@ class HoloSphere {
304
258
  resolve(); // Resolve the promise to prevent it from hanging
305
259
  }, 1000); // Timeout of 5 seconds
306
260
 
307
- this.db.gun.get(siblings[i]).get(lense).map().once((data, key) => {
261
+ this.gun.get(siblings[i]).get(lense).map().once((data, key) => {
308
262
  clearTimeout(timeout); // Clear the timeout if data is received
309
263
  if (data) {
310
264
  content.push(data.content);
@@ -324,6 +278,11 @@ class HoloSphere {
324
278
  this.compute(parent, lense, operation)
325
279
  }
326
280
 
281
+ /**
282
+ * Clears all entities under a specific hex and lense.
283
+ * @param {string} hex - The hex identifier.
284
+ * @param {string} lense - The lense to clear.
285
+ */
327
286
  async clearlense(hex, lense) {
328
287
  let entities = {};
329
288
 
@@ -336,6 +295,11 @@ class HoloSphere {
336
295
  }
337
296
 
338
297
 
298
+ /**
299
+ * Summarizes provided history text using OpenAI.
300
+ * @param {string} history - The history text to summarize.
301
+ * @returns {Promise<string>} - The summarized text.
302
+ */
339
303
  async summarize(history) {
340
304
  if (!this.openai) {
341
305
  return 'OpenAI not initialized, please specify the API key in the constructor.'
@@ -368,21 +332,34 @@ class HoloSphere {
368
332
  return summary
369
333
  }
370
334
 
335
+ /**
336
+ * Upcasts content to parent hexagons recursively.
337
+ * @param {string} hex - The current hex identifier.
338
+ * @param {string} lense - The lense under which to upcast.
339
+ * @param {object} content - The content to upcast.
340
+ * @returns {Promise<object>} - The upcasted content.
341
+ */
371
342
  async upcast(hex, lense, content) {
372
343
  let res = h3.getResolution(hex)
373
-
374
-
375
- console.log('Upcasting ', hex, lense, content)
376
- let parent = h3.cellToParent(hex, res - 1)
377
- await this.put(parent, lense, content)
378
- if (res == 0)
344
+ if (res == 0) {
345
+ await this.putNode(hex, lense, content)
379
346
  return content
380
- else
347
+ }
348
+ else {
349
+ console.log('Upcasting ', hex, lense, content, res)
350
+ await this.putNode(hex, lense, content)
351
+ let parent = h3.cellToParent(hex, res - 1)
381
352
  return this.upcast(parent, lense, content)
353
+ }
382
354
  }
383
355
 
384
356
 
385
- // send information upwards, triggers the parent to update its summary
357
+ /**
358
+ * Updates the parent hexagon with a new report.
359
+ * @param {string} id - The child hex identifier.
360
+ * @param {string} report - The report to update.
361
+ * @returns {Promise<object>} - The updated parent information.
362
+ */
386
363
  async updateParent(id, report) {
387
364
  let cellinfo = await this.getCellInfo(id)
388
365
  let res = h3.getResolution(id)
@@ -398,11 +375,23 @@ class HoloSphere {
398
375
  }
399
376
 
400
377
 
378
+ /**
379
+ * Converts latitude and longitude to a hex identifier.
380
+ * @param {number} lat - The latitude.
381
+ * @param {number} lng - The longitude.
382
+ * @param {number} resolution - The resolution level.
383
+ * @returns {Promise<string>} - The resulting hex identifier.
384
+ */
401
385
  async getHex(lat, lng, resolution) {
402
386
  return h3.latLngToCell(lat, lng, resolution);
403
387
  }
404
388
 
405
- // returns the list of all the containing hexagons at xall scales
389
+ /**
390
+ * Retrieves all containing hexagons at all scales for given coordinates.
391
+ * @param {number} lat - The latitude.
392
+ * @param {number} lng - The longitude.
393
+ * @returns {Array<string>} - List of hex identifiers.
394
+ */
406
395
  getScalespace(lat, lng) {
407
396
  let list = []
408
397
  let cell = h3.latLngToCell(lat, lng, 14);
@@ -413,7 +402,11 @@ class HoloSphere {
413
402
  return list
414
403
  }
415
404
 
416
- // returns the list of all the containing hexagons at xall scales
405
+ /**
406
+ * Retrieves all containing hexagons at all scales for a given hex.
407
+ * @param {string} hex - The hex identifier.
408
+ * @returns {Array<string>} - List of hex identifiers.
409
+ */
417
410
  getHexScalespace(hex) {
418
411
  let list = []
419
412
  let res = h3.getResolution(hex)
@@ -423,43 +416,73 @@ class HoloSphere {
423
416
  return list
424
417
  }
425
418
 
419
+ /**
420
+ * Subscribes to changes in a specific hex and lense.
421
+ * @param {string} hex - The hex identifier.
422
+ * @param {string} lense - The lense to subscribe to.
423
+ * @param {function} callback - The callback to execute on changes.
424
+ */
426
425
  subscribe(hex, lense, callback) {
427
426
  this.gun.get(hex).get(lense).map().on((data, key) => {
428
427
  callback(data, key)
429
428
  })
430
429
  }
431
430
 
432
- // VOTING SYSTEM
431
+ /**
432
+ * Retrieves the final vote for a user, considering delegations.
433
+ * @param {string} userId - The user's identifier.
434
+ * @param {string} topic - The voting topic.
435
+ * @param {object} votes - The current votes.
436
+ * @param {Set<string>} [visited=new Set()] - Set of visited users to prevent cycles.
437
+ * @returns {string|null} - The final vote or null if not found.
438
+ */
439
+ getFinalVote(userId, topic, votes, visited = new Set()) {
440
+ if (this.users[userId]) { // Added this.users
441
+ if (visited.has(userId)) {
442
+ return null; // Avoid circular delegations
443
+ }
444
+ visited.add(userId);
433
445
 
434
- getFinalVote(userId, topic, votes, visited = new Set()) {
435
- if (visited.has(userId)) {
436
- return null; // Avoid circular delegations
437
- }
438
- visited.add(userId);
446
+ const delegation = this.users[userId].delegations[topic];
447
+ if (delegation && votes[delegation] === undefined) {
448
+ return this.getFinalVote(delegation, topic, votes, visited); // Prefixed with this
449
+ }
439
450
 
440
- const delegation = users[userId].delegations[topic];
441
- if (delegation && votes[delegation] === undefined) {
442
- return getFinalVote(delegation, topic, votes, visited); // Follow delegation
443
- }
451
+ return votes[userId] !== undefined ? votes[userId] : null;
452
+ }
453
+ return null;
454
+ }
444
455
 
445
- return votes[userId] !== undefined ? votes[userId] : null; // Return direct vote or null
446
- }
456
+ /**
457
+ * Aggregates votes for a specific hex and topic.
458
+ * @param {string} hexId - The hex identifier.
459
+ * @param {string} topic - The voting topic.
460
+ * @returns {object} - Aggregated vote counts.
461
+ */
462
+ aggregateVotes(hexId, topic) {
463
+ if (!this.hexagonVotes[hexId] || !this.hexagonVotes[hexId][topic]) {
464
+ return {}; // Handle undefined votes
465
+ }
466
+ const votes = this.hexagonVotes[hexId][topic];
467
+ const aggregatedVotes = {};
447
468
 
448
- aggregateVotes(hexId, topic) {
449
- const votes = hexagonVotes[hexId][topic];
450
- const aggregatedVotes = {};
469
+ Object.keys(votes).forEach(userId => {
470
+ const finalVote = this.getFinalVote(userId, topic, votes); // Prefixed with this
471
+ if (finalVote !== null) {
472
+ aggregatedVotes[finalVote] = (aggregatedVotes[finalVote] || 0) + 1;
473
+ }
474
+ });
451
475
 
452
- Object.keys(votes).forEach(userId => {
453
- const finalVote = getFinalVote(userId, topic, votes);
454
- if (finalVote !== null) {
455
- aggregatedVotes[finalVote] = (aggregatedVotes[finalVote] || 0) + 1;
476
+ return aggregatedVotes;
456
477
  }
457
- });
458
-
459
- return aggregatedVotes;
460
- }
461
478
 
462
- async delegateVote(userId, topic, delegateTo) {
479
+ /**
480
+ * Delegates a user's vote to another user.
481
+ * @param {string} userId - The user's identifier.
482
+ * @param {string} topic - The voting topic.
483
+ * @param {string} delegateTo - The user to delegate the vote to.
484
+ */
485
+ async delegateVote(userId, topic, delegateTo) {
463
486
  const response = await fetch('/delegate', {
464
487
  method: 'POST',
465
488
  headers: { 'Content-Type': 'application/json' },
@@ -468,6 +491,13 @@ async delegateVote(userId, topic, delegateTo) {
468
491
  alert(await response.text());
469
492
  }
470
493
 
494
+ /**
495
+ * Casts a vote for a user on a specific topic and hex.
496
+ * @param {string} userId - The user's identifier.
497
+ * @param {string} hexId - The hex identifier.
498
+ * @param {string} topic - The voting topic.
499
+ * @param {string} vote - The vote choice.
500
+ */
471
501
  async vote(userId, hexId, topic, vote) {
472
502
  const response = await fetch('/vote', {
473
503
  method: 'POST',
@@ -478,6 +508,8 @@ async delegateVote(userId, topic, delegateTo) {
478
508
  }
479
509
 
480
510
 
511
+
512
+
481
513
  }
482
514
 
483
515
  export default HoloSphere;
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "holosphere",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Holonic Geospatial Communication Infrastructure based on h3.js and gun.js",
5
- "main": "index.js",
5
+ "main": "holosphere.js",
6
6
  "type": "module",
7
7
  "scripts": {
8
8
  "test": "echo \"Error: no test specified\" && exit 1"