@happyvertical/smrt-places 0.30.0
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/AGENTS.md +29 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +7 -0
- package/README.md +185 -0
- package/dist/index.d.ts +606 -0
- package/dist/index.js +966 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.json +2382 -0
- package/dist/smrt-knowledge.json +900 -0
- package/dist/types.d.ts +172 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +113 -0
- package/dist/utils.js +110 -0
- package/dist/utils.js.map +1 -0
- package/package.json +76 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,966 @@
|
|
|
1
|
+
import { ObjectRegistry, foreignKey, crossPackageRef, field, smrt, SmrtObject, SmrtJunction, SmrtHierarchical, SmrtCollection } from "@happyvertical/smrt-core";
|
|
2
|
+
import { getOwnedAssetsFromCollection, addOwnedAssetFromCollection, removeOwnedAssetFromCollection, resolveOwnedAssetsById, assertValidOwnedAssetRelationship, assertValidOwnedAssetSortOrder } from "@happyvertical/smrt-assets";
|
|
3
|
+
import { tenantId, TenantScoped } from "@happyvertical/smrt-tenancy";
|
|
4
|
+
import { getGeoAdapter } from "@happyvertical/geo";
|
|
5
|
+
import { areCoordinatesNear, calculateDistance, formatCoordinates, generateDisplayName, locationToGeoData, mapLocationTypeToPlaceType, normalizeAddressComponents, parseCoordinates, validateCoordinates } from "./utils.js";
|
|
6
|
+
ObjectRegistry.registerPackageManifest(
|
|
7
|
+
new URL("./manifest.json", import.meta.url)
|
|
8
|
+
);
|
|
9
|
+
var __defProp$3 = Object.defineProperty;
|
|
10
|
+
var __getOwnPropDesc$3 = Object.getOwnPropertyDescriptor;
|
|
11
|
+
var __decorateClass$3 = (decorators, target, key, kind) => {
|
|
12
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$3(target, key) : target;
|
|
13
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
14
|
+
if (decorator = decorators[i])
|
|
15
|
+
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
16
|
+
if (kind && result) __defProp$3(target, key, result);
|
|
17
|
+
return result;
|
|
18
|
+
};
|
|
19
|
+
let PlaceAsset = class extends SmrtObject {
|
|
20
|
+
tenantId = null;
|
|
21
|
+
placeId = "";
|
|
22
|
+
assetId = "";
|
|
23
|
+
relationship = "attachment";
|
|
24
|
+
sortOrder = 0;
|
|
25
|
+
constructor(options = {}) {
|
|
26
|
+
super(options);
|
|
27
|
+
if (options.placeId) this.placeId = options.placeId;
|
|
28
|
+
if (options.assetId) this.assetId = options.assetId;
|
|
29
|
+
if (options.relationship) this.relationship = options.relationship;
|
|
30
|
+
if (options.sortOrder !== void 0) this.sortOrder = options.sortOrder;
|
|
31
|
+
if (options.tenantId !== void 0) this.tenantId = options.tenantId;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
__decorateClass$3([
|
|
35
|
+
tenantId({ nullable: true })
|
|
36
|
+
], PlaceAsset.prototype, "tenantId", 2);
|
|
37
|
+
__decorateClass$3([
|
|
38
|
+
foreignKey("Place", { required: true })
|
|
39
|
+
], PlaceAsset.prototype, "placeId", 2);
|
|
40
|
+
__decorateClass$3([
|
|
41
|
+
crossPackageRef("@happyvertical/smrt-assets:Asset", { required: true })
|
|
42
|
+
], PlaceAsset.prototype, "assetId", 2);
|
|
43
|
+
__decorateClass$3([
|
|
44
|
+
field({ required: true })
|
|
45
|
+
], PlaceAsset.prototype, "relationship", 2);
|
|
46
|
+
__decorateClass$3([
|
|
47
|
+
field()
|
|
48
|
+
], PlaceAsset.prototype, "sortOrder", 2);
|
|
49
|
+
PlaceAsset = __decorateClass$3([
|
|
50
|
+
TenantScoped({ mode: "optional" }),
|
|
51
|
+
smrt({
|
|
52
|
+
tableName: "place_assets",
|
|
53
|
+
conflictColumns: ["place_id", "asset_id", "relationship"],
|
|
54
|
+
api: false,
|
|
55
|
+
mcp: false,
|
|
56
|
+
cli: false
|
|
57
|
+
})
|
|
58
|
+
], PlaceAsset);
|
|
59
|
+
var __defProp$2 = Object.defineProperty;
|
|
60
|
+
var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor;
|
|
61
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp$2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
62
|
+
var __decorateClass$2 = (decorators, target, key, kind) => {
|
|
63
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target;
|
|
64
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
65
|
+
if (decorator = decorators[i])
|
|
66
|
+
result = decorator(result) || result;
|
|
67
|
+
return result;
|
|
68
|
+
};
|
|
69
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, key + "", value);
|
|
70
|
+
let PlaceAssetCollection = class extends SmrtJunction {
|
|
71
|
+
leftField = "placeId";
|
|
72
|
+
rightField = "assetId";
|
|
73
|
+
placeCollectionPromise = null;
|
|
74
|
+
async getPlaceCollection() {
|
|
75
|
+
if (!this.placeCollectionPromise) {
|
|
76
|
+
const { PlaceCollection: PlaceCollection2 } = await Promise.resolve().then(() => PlaceCollection$1);
|
|
77
|
+
this.placeCollectionPromise = PlaceCollection2.create({ db: this.db });
|
|
78
|
+
}
|
|
79
|
+
return this.placeCollectionPromise;
|
|
80
|
+
}
|
|
81
|
+
async getAssets(placeId, relationship) {
|
|
82
|
+
return getOwnedAssetsFromCollection(
|
|
83
|
+
await this.getPlaceCollection(),
|
|
84
|
+
placeId,
|
|
85
|
+
relationship
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
async addAsset(placeId, asset, relationship = "attachment", sortOrder = 0) {
|
|
89
|
+
await addOwnedAssetFromCollection(
|
|
90
|
+
await this.getPlaceCollection(),
|
|
91
|
+
"Place",
|
|
92
|
+
placeId,
|
|
93
|
+
asset,
|
|
94
|
+
relationship,
|
|
95
|
+
sortOrder
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
async removeAsset(placeId, assetId, relationship) {
|
|
99
|
+
await removeOwnedAssetFromCollection(
|
|
100
|
+
await this.getPlaceCollection(),
|
|
101
|
+
"Place",
|
|
102
|
+
placeId,
|
|
103
|
+
assetId,
|
|
104
|
+
relationship
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
__publicField(PlaceAssetCollection, "_itemClass", PlaceAsset);
|
|
109
|
+
PlaceAssetCollection = __decorateClass$2([
|
|
110
|
+
smrt({
|
|
111
|
+
api: false,
|
|
112
|
+
mcp: false,
|
|
113
|
+
cli: false
|
|
114
|
+
})
|
|
115
|
+
], PlaceAssetCollection);
|
|
116
|
+
const PlaceAssetCollection$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
117
|
+
__proto__: null,
|
|
118
|
+
get PlaceAssetCollection() {
|
|
119
|
+
return PlaceAssetCollection;
|
|
120
|
+
}
|
|
121
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
122
|
+
var __defProp$1 = Object.defineProperty;
|
|
123
|
+
var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
|
|
124
|
+
var __decorateClass$1 = (decorators, target, key, kind) => {
|
|
125
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
|
|
126
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
127
|
+
if (decorator = decorators[i])
|
|
128
|
+
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
129
|
+
if (kind && result) __defProp$1(target, key, result);
|
|
130
|
+
return result;
|
|
131
|
+
};
|
|
132
|
+
let Place = class extends SmrtHierarchical {
|
|
133
|
+
tenantId = null;
|
|
134
|
+
typeId = "";
|
|
135
|
+
// FK to PlaceType
|
|
136
|
+
// parentId inherited from SmrtHierarchical (nullable, self-reference)
|
|
137
|
+
name = "";
|
|
138
|
+
// Place name/title
|
|
139
|
+
description = "";
|
|
140
|
+
latitude = null;
|
|
141
|
+
longitude = null;
|
|
142
|
+
streetNumber = "";
|
|
143
|
+
streetName = "";
|
|
144
|
+
city = "";
|
|
145
|
+
region = "";
|
|
146
|
+
country = "";
|
|
147
|
+
postalCode = "";
|
|
148
|
+
countryCode = "";
|
|
149
|
+
timezone = "";
|
|
150
|
+
// Metadata
|
|
151
|
+
externalId = "";
|
|
152
|
+
// External system identifier
|
|
153
|
+
source = "";
|
|
154
|
+
// Where this place came from (e.g., 'google', 'osm')
|
|
155
|
+
metadata = "";
|
|
156
|
+
// JSON metadata stored as text
|
|
157
|
+
// Timestamps
|
|
158
|
+
createdAt = /* @__PURE__ */ new Date();
|
|
159
|
+
updatedAt = /* @__PURE__ */ new Date();
|
|
160
|
+
constructor(options = {}) {
|
|
161
|
+
super(options);
|
|
162
|
+
if (options.tenantId !== void 0) this.tenantId = options.tenantId;
|
|
163
|
+
if (options.typeId) this.typeId = options.typeId;
|
|
164
|
+
if (options.parentId !== void 0)
|
|
165
|
+
this.parentId = options.parentId ?? null;
|
|
166
|
+
if (options.name) this.name = options.name;
|
|
167
|
+
if (options.description !== void 0)
|
|
168
|
+
this.description = options.description;
|
|
169
|
+
if (options.latitude !== void 0) this.latitude = options.latitude;
|
|
170
|
+
if (options.longitude !== void 0) this.longitude = options.longitude;
|
|
171
|
+
if (options.streetNumber !== void 0)
|
|
172
|
+
this.streetNumber = options.streetNumber;
|
|
173
|
+
if (options.streetName !== void 0) this.streetName = options.streetName;
|
|
174
|
+
if (options.city !== void 0) this.city = options.city;
|
|
175
|
+
if (options.region !== void 0) this.region = options.region;
|
|
176
|
+
if (options.country !== void 0) this.country = options.country;
|
|
177
|
+
if (options.postalCode !== void 0) this.postalCode = options.postalCode;
|
|
178
|
+
if (options.countryCode !== void 0)
|
|
179
|
+
this.countryCode = options.countryCode;
|
|
180
|
+
if (options.timezone !== void 0) this.timezone = options.timezone;
|
|
181
|
+
if (options.externalId !== void 0) this.externalId = options.externalId;
|
|
182
|
+
if (options.source !== void 0) this.source = options.source;
|
|
183
|
+
if (options.metadata !== void 0) {
|
|
184
|
+
if (typeof options.metadata === "string") {
|
|
185
|
+
this.metadata = options.metadata;
|
|
186
|
+
} else {
|
|
187
|
+
this.metadata = JSON.stringify(options.metadata);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (options.createdAt) this.createdAt = options.createdAt;
|
|
191
|
+
if (options.updatedAt) this.updatedAt = options.updatedAt;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Get geographic data for this place
|
|
195
|
+
*
|
|
196
|
+
* @returns GeoData object with all geo fields
|
|
197
|
+
*/
|
|
198
|
+
getGeoData() {
|
|
199
|
+
return {
|
|
200
|
+
latitude: this.latitude,
|
|
201
|
+
longitude: this.longitude,
|
|
202
|
+
streetNumber: this.streetNumber || void 0,
|
|
203
|
+
streetName: this.streetName || void 0,
|
|
204
|
+
city: this.city || void 0,
|
|
205
|
+
region: this.region || void 0,
|
|
206
|
+
country: this.country || void 0,
|
|
207
|
+
postalCode: this.postalCode || void 0,
|
|
208
|
+
countryCode: this.countryCode || void 0,
|
|
209
|
+
timezone: this.timezone || void 0
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Check if this place has geographic coordinates
|
|
214
|
+
*
|
|
215
|
+
* @returns True if latitude and longitude are set
|
|
216
|
+
*/
|
|
217
|
+
hasCoordinates() {
|
|
218
|
+
return this.latitude !== null && this.longitude !== null;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Get metadata as parsed object
|
|
222
|
+
*
|
|
223
|
+
* @returns Parsed metadata object or empty object if no metadata
|
|
224
|
+
*/
|
|
225
|
+
getMetadata() {
|
|
226
|
+
if (!this.metadata) return {};
|
|
227
|
+
try {
|
|
228
|
+
return JSON.parse(this.metadata);
|
|
229
|
+
} catch {
|
|
230
|
+
return {};
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Set metadata from object
|
|
235
|
+
*
|
|
236
|
+
* @param data - Metadata object to store
|
|
237
|
+
*/
|
|
238
|
+
setMetadata(data) {
|
|
239
|
+
this.metadata = JSON.stringify(data);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Update metadata by merging with existing values
|
|
243
|
+
*
|
|
244
|
+
* @param updates - Partial metadata to merge
|
|
245
|
+
*/
|
|
246
|
+
updateMetadata(updates) {
|
|
247
|
+
const current = this.getMetadata();
|
|
248
|
+
this.setMetadata({ ...current, ...updates });
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Get the place type
|
|
252
|
+
*
|
|
253
|
+
* @returns PlaceType instance or null if not found
|
|
254
|
+
*/
|
|
255
|
+
async getType() {
|
|
256
|
+
if (!this.typeId) return null;
|
|
257
|
+
const { PlaceTypeCollection: PlaceTypeCollection2 } = await Promise.resolve().then(() => PlaceTypeCollection$1);
|
|
258
|
+
const collection = await PlaceTypeCollection2.create(this.options);
|
|
259
|
+
return await collection.get({ id: this.typeId });
|
|
260
|
+
}
|
|
261
|
+
// Hierarchy traversal (getParent / getChildren / getAncestors /
|
|
262
|
+
// getDescendants / getHierarchy / moveTo) provided by SmrtHierarchical.
|
|
263
|
+
async getPlaceAssetCollection() {
|
|
264
|
+
const { PlaceAssetCollection: PlaceAssetCollection2 } = await Promise.resolve().then(() => PlaceAssetCollection$1);
|
|
265
|
+
return PlaceAssetCollection2.create({ db: this.db });
|
|
266
|
+
}
|
|
267
|
+
async getAssets(relationship) {
|
|
268
|
+
if (!this.id) {
|
|
269
|
+
return [];
|
|
270
|
+
}
|
|
271
|
+
const placeAssets = await this.getPlaceAssetCollection();
|
|
272
|
+
const linkedAssets = await placeAssets.byLeft(
|
|
273
|
+
this.id,
|
|
274
|
+
relationship ? { relationship } : {}
|
|
275
|
+
);
|
|
276
|
+
return resolveOwnedAssetsById(
|
|
277
|
+
this.db,
|
|
278
|
+
linkedAssets.map((link) => link.assetId),
|
|
279
|
+
this.tenantId
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
async addAsset(asset, relationship = "attachment", sortOrder = 0) {
|
|
283
|
+
if (!this.id || !asset.id) {
|
|
284
|
+
throw new Error("Cannot associate unsaved place or asset");
|
|
285
|
+
}
|
|
286
|
+
assertValidOwnedAssetRelationship(relationship);
|
|
287
|
+
assertValidOwnedAssetSortOrder(sortOrder);
|
|
288
|
+
const placeAssets = await this.getPlaceAssetCollection();
|
|
289
|
+
await placeAssets.attach(this.id, asset.id, {
|
|
290
|
+
relationship,
|
|
291
|
+
sortOrder,
|
|
292
|
+
tenantId: this.tenantId
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
async removeAsset(assetId, relationship) {
|
|
296
|
+
if (!this.id) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const placeAssets = await this.getPlaceAssetCollection();
|
|
300
|
+
await placeAssets.detach(
|
|
301
|
+
this.id,
|
|
302
|
+
assetId,
|
|
303
|
+
relationship ? { relationship } : {}
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
__decorateClass$1([
|
|
308
|
+
tenantId({ nullable: true })
|
|
309
|
+
], Place.prototype, "tenantId", 2);
|
|
310
|
+
__decorateClass$1([
|
|
311
|
+
foreignKey("PlaceType")
|
|
312
|
+
], Place.prototype, "typeId", 2);
|
|
313
|
+
__decorateClass$1([
|
|
314
|
+
field({ type: "decimal" })
|
|
315
|
+
], Place.prototype, "latitude", 2);
|
|
316
|
+
__decorateClass$1([
|
|
317
|
+
field({ type: "decimal" })
|
|
318
|
+
], Place.prototype, "longitude", 2);
|
|
319
|
+
Place = __decorateClass$1([
|
|
320
|
+
TenantScoped({ mode: "optional" }),
|
|
321
|
+
smrt({
|
|
322
|
+
tableStrategy: "sti",
|
|
323
|
+
api: { include: ["list", "get", "create", "update", "delete"] },
|
|
324
|
+
mcp: { include: ["list", "get", "create", "update"] },
|
|
325
|
+
cli: true
|
|
326
|
+
})
|
|
327
|
+
], Place);
|
|
328
|
+
var __defProp = Object.defineProperty;
|
|
329
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
330
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
331
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
332
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
333
|
+
if (decorator = decorators[i])
|
|
334
|
+
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
335
|
+
if (kind && result) __defProp(target, key, result);
|
|
336
|
+
return result;
|
|
337
|
+
};
|
|
338
|
+
let PlaceType = class extends SmrtObject {
|
|
339
|
+
name = "";
|
|
340
|
+
// Type name (e.g., 'Town', 'City', 'Country')
|
|
341
|
+
description;
|
|
342
|
+
// Optional description
|
|
343
|
+
// Timestamps
|
|
344
|
+
createdAt = /* @__PURE__ */ new Date();
|
|
345
|
+
updatedAt = /* @__PURE__ */ new Date();
|
|
346
|
+
constructor(options = {}) {
|
|
347
|
+
super(options);
|
|
348
|
+
if (options.name) this.name = options.name;
|
|
349
|
+
if (options.description !== void 0)
|
|
350
|
+
this.description = options.description;
|
|
351
|
+
if (options.createdAt) this.createdAt = options.createdAt;
|
|
352
|
+
if (options.updatedAt) this.updatedAt = options.updatedAt;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Convenience method for slug-based lookup
|
|
356
|
+
*
|
|
357
|
+
* @param slug - The slug to search for
|
|
358
|
+
* @returns PlaceType instance or null if not found
|
|
359
|
+
*/
|
|
360
|
+
static async getBySlug(_slug) {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
__decorateClass([
|
|
365
|
+
field({ required: true })
|
|
366
|
+
], PlaceType.prototype, "name", 2);
|
|
367
|
+
PlaceType = __decorateClass([
|
|
368
|
+
smrt({
|
|
369
|
+
tableStrategy: "sti",
|
|
370
|
+
api: { include: ["list", "get", "create", "update", "delete"] },
|
|
371
|
+
mcp: { include: ["list", "get", "create"] },
|
|
372
|
+
cli: true
|
|
373
|
+
})
|
|
374
|
+
], PlaceType);
|
|
375
|
+
class PlaceTypeCollection extends SmrtCollection {
|
|
376
|
+
static _itemClass = PlaceType;
|
|
377
|
+
/**
|
|
378
|
+
* Get or create a place type by slug
|
|
379
|
+
*
|
|
380
|
+
* @param slug - PlaceType slug (e.g., 'city', 'building')
|
|
381
|
+
* @param name - Optional display name (defaults to capitalized slug)
|
|
382
|
+
* @returns PlaceType instance
|
|
383
|
+
*/
|
|
384
|
+
async getOrCreate(slug, name) {
|
|
385
|
+
const existing = await this.get({ slug });
|
|
386
|
+
if (existing) {
|
|
387
|
+
return existing;
|
|
388
|
+
}
|
|
389
|
+
const displayName = name || slug.replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
|
|
390
|
+
return await this.create({
|
|
391
|
+
slug,
|
|
392
|
+
name: displayName
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Get a place type by slug
|
|
397
|
+
*
|
|
398
|
+
* @param slug - PlaceType slug to search for
|
|
399
|
+
* @returns PlaceType instance or null if not found
|
|
400
|
+
*/
|
|
401
|
+
async getBySlug(slug) {
|
|
402
|
+
return await this.get({ slug });
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Initialize default place types
|
|
406
|
+
*
|
|
407
|
+
* Creates standard types if they don't exist:
|
|
408
|
+
* - country
|
|
409
|
+
* - region (state/province)
|
|
410
|
+
* - city
|
|
411
|
+
* - address
|
|
412
|
+
* - building
|
|
413
|
+
* - room
|
|
414
|
+
* - zone (for abstract/virtual places)
|
|
415
|
+
*
|
|
416
|
+
* @returns Array of created/existing place types
|
|
417
|
+
*/
|
|
418
|
+
async initializeDefaults() {
|
|
419
|
+
const defaults = [
|
|
420
|
+
{ slug: "country", name: "Country" },
|
|
421
|
+
{ slug: "region", name: "Region" },
|
|
422
|
+
{ slug: "city", name: "City" },
|
|
423
|
+
{ slug: "address", name: "Address" },
|
|
424
|
+
{ slug: "building", name: "Building" },
|
|
425
|
+
{ slug: "room", name: "Room" },
|
|
426
|
+
{ slug: "zone", name: "Zone" },
|
|
427
|
+
{ slug: "point_of_interest", name: "Point of Interest" }
|
|
428
|
+
];
|
|
429
|
+
const types = [];
|
|
430
|
+
for (const def of defaults) {
|
|
431
|
+
const type = await this.getOrCreate(def.slug, def.name);
|
|
432
|
+
types.push(type);
|
|
433
|
+
}
|
|
434
|
+
return types;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
const PlaceTypeCollection$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
438
|
+
__proto__: null,
|
|
439
|
+
PlaceTypeCollection
|
|
440
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
441
|
+
class PlaceCollection extends SmrtCollection {
|
|
442
|
+
static _itemClass = Place;
|
|
443
|
+
/**
|
|
444
|
+
* Look up a place by query or coordinates, creating it if not found
|
|
445
|
+
*
|
|
446
|
+
* This is the key method for organic database growth:
|
|
447
|
+
* 1. Search local database first
|
|
448
|
+
* 2. If not found, query @happyvertical/geo
|
|
449
|
+
* 3. Create place from geocoding result
|
|
450
|
+
* 4. Return place
|
|
451
|
+
*
|
|
452
|
+
* @param query - Address or location query string
|
|
453
|
+
* @param options - Lookup options (provider, type, parent, etc.)
|
|
454
|
+
* @returns Place instance
|
|
455
|
+
*/
|
|
456
|
+
async lookupOrCreate(query, options = {}) {
|
|
457
|
+
const {
|
|
458
|
+
geoProvider = "openstreetmap",
|
|
459
|
+
typeSlug,
|
|
460
|
+
parentId,
|
|
461
|
+
createIfNotFound = true,
|
|
462
|
+
coords
|
|
463
|
+
} = options;
|
|
464
|
+
let existingPlace = null;
|
|
465
|
+
if (coords) {
|
|
466
|
+
existingPlace = await this.findByCoordinates(coords.lat, coords.lng);
|
|
467
|
+
}
|
|
468
|
+
if (!existingPlace) {
|
|
469
|
+
existingPlace = await this.findByQuery(query);
|
|
470
|
+
}
|
|
471
|
+
if (existingPlace) {
|
|
472
|
+
return existingPlace;
|
|
473
|
+
}
|
|
474
|
+
if (!createIfNotFound) {
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
const locations = await this.geocode(
|
|
478
|
+
query,
|
|
479
|
+
coords,
|
|
480
|
+
geoProvider
|
|
481
|
+
);
|
|
482
|
+
if (locations.length === 0) {
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
const location = locations[0];
|
|
486
|
+
return await this.createFromLocation(location, typeSlug, parentId);
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Find place by coordinates (within small threshold)
|
|
490
|
+
*
|
|
491
|
+
* @param latitude - Latitude to search
|
|
492
|
+
* @param longitude - Longitude to search
|
|
493
|
+
* @param threshold - Max distance in degrees (default: 0.0001 ~11m)
|
|
494
|
+
* @returns Place instance or null
|
|
495
|
+
*/
|
|
496
|
+
async findByCoordinates(latitude, longitude, threshold = 1e-4) {
|
|
497
|
+
const places = await this.list({
|
|
498
|
+
where: {
|
|
499
|
+
latitude: { $ne: null },
|
|
500
|
+
longitude: { $ne: null }
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
for (const place of places) {
|
|
504
|
+
const placeObj = place;
|
|
505
|
+
if (placeObj.latitude === null || placeObj.longitude === null) continue;
|
|
506
|
+
const latDiff = Math.abs(placeObj.latitude - latitude);
|
|
507
|
+
const lngDiff = Math.abs(placeObj.longitude - longitude);
|
|
508
|
+
if (latDiff < threshold && lngDiff < threshold) {
|
|
509
|
+
return place;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Find place by query text (matches name, city, region, country)
|
|
516
|
+
*
|
|
517
|
+
* @param query - Search query
|
|
518
|
+
* @returns Place instance or null
|
|
519
|
+
*/
|
|
520
|
+
async findByQuery(query) {
|
|
521
|
+
const normalizedQuery = query.toLowerCase().trim();
|
|
522
|
+
const places = await this.list({});
|
|
523
|
+
for (const place of places) {
|
|
524
|
+
if (place.name.toLowerCase().includes(normalizedQuery)) {
|
|
525
|
+
return place;
|
|
526
|
+
}
|
|
527
|
+
const addressParts = [
|
|
528
|
+
place.streetNumber,
|
|
529
|
+
place.streetName,
|
|
530
|
+
place.city,
|
|
531
|
+
place.region,
|
|
532
|
+
place.country
|
|
533
|
+
].filter((p) => p).join(" ").toLowerCase();
|
|
534
|
+
if (addressParts.includes(normalizedQuery)) {
|
|
535
|
+
return place;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Geocode query or coordinates using @happyvertical/geo
|
|
542
|
+
*
|
|
543
|
+
* @param query - Address query
|
|
544
|
+
* @param coords - Optional coordinates for reverse geocoding
|
|
545
|
+
* @param provider - Geo provider to use
|
|
546
|
+
* @returns Array of Location results
|
|
547
|
+
*/
|
|
548
|
+
async geocode(query, coords, provider = "openstreetmap") {
|
|
549
|
+
const geo = await this.getGeoAdapter(provider);
|
|
550
|
+
if (coords) {
|
|
551
|
+
return await geo.reverseGeocode(coords.lat, coords.lng);
|
|
552
|
+
}
|
|
553
|
+
return await geo.lookup(query);
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Find a place by the provider's native id (Google place_id, OSM
|
|
557
|
+
* osm-node-N, etc.). Used as the primary idempotency key by
|
|
558
|
+
* `discoverNearby` so repeat POI searches don't create duplicate rows.
|
|
559
|
+
*/
|
|
560
|
+
async findByExternalId(externalId) {
|
|
561
|
+
if (!externalId) return null;
|
|
562
|
+
const matches = await this.list({
|
|
563
|
+
where: { externalId },
|
|
564
|
+
limit: 1
|
|
565
|
+
});
|
|
566
|
+
return matches[0] ?? null;
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Idempotent "find or create" for a geo Location. Keyed purely on the
|
|
570
|
+
* provider's externalId (Google place_id, osm-node-*, etc.), which is
|
|
571
|
+
* stable per POI across repeat searches. The older `lookupOrCreate`
|
|
572
|
+
* keeps its own fallback chain (name/address/coordinate matching) for
|
|
573
|
+
* address-style workflows where externalId isn't reliable.
|
|
574
|
+
*
|
|
575
|
+
* Returns `{ place, created }` so callers can distinguish fresh rows
|
|
576
|
+
* from reused ones without re-querying the table — this is how
|
|
577
|
+
* `resolveTrackPlaces` derives its `cacheHitCount` without a full
|
|
578
|
+
* scan per bucket.
|
|
579
|
+
*/
|
|
580
|
+
async ensureFromLocation(location, typeSlug, parentId) {
|
|
581
|
+
if (location.id) {
|
|
582
|
+
const existing = await this.findByExternalId(location.id);
|
|
583
|
+
if (existing) return { place: existing, created: false };
|
|
584
|
+
}
|
|
585
|
+
const place = await this.createFromLocation(location, typeSlug, parentId);
|
|
586
|
+
return { place, created: true };
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Create place from @happyvertical/geo Location data
|
|
590
|
+
*
|
|
591
|
+
* @param location - Location from geocoding
|
|
592
|
+
* @param typeSlug - Optional type slug override
|
|
593
|
+
* @param parentId - Optional parent place ID
|
|
594
|
+
* @returns Created Place instance
|
|
595
|
+
*/
|
|
596
|
+
async createFromLocation(location, typeSlug, parentId) {
|
|
597
|
+
const typeCollection = await PlaceTypeCollection.create(
|
|
598
|
+
this.options
|
|
599
|
+
);
|
|
600
|
+
const slug = typeSlug || location.type || "address";
|
|
601
|
+
const placeType = await typeCollection.getOrCreate(slug);
|
|
602
|
+
const components = location.addressComponents || {};
|
|
603
|
+
return await this.create({
|
|
604
|
+
typeId: placeType.id,
|
|
605
|
+
parentId: parentId ?? null,
|
|
606
|
+
name: location.name,
|
|
607
|
+
description: "",
|
|
608
|
+
// Geo fields from location
|
|
609
|
+
latitude: location.latitude,
|
|
610
|
+
longitude: location.longitude,
|
|
611
|
+
streetNumber: components.streetNumber || "",
|
|
612
|
+
streetName: components.streetName || "",
|
|
613
|
+
city: components.city || "",
|
|
614
|
+
region: components.region || "",
|
|
615
|
+
country: components.country || "",
|
|
616
|
+
postalCode: components.postalCode || "",
|
|
617
|
+
countryCode: location.countryCode || "",
|
|
618
|
+
timezone: location.timezone || "",
|
|
619
|
+
// Metadata
|
|
620
|
+
externalId: location.id,
|
|
621
|
+
source: location.raw?.provider || "unknown",
|
|
622
|
+
metadata: JSON.stringify({ raw: location.raw ?? null })
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Discover POIs near a coordinate and persist them as Place rows.
|
|
627
|
+
*
|
|
628
|
+
* Composes `@happyvertical/geo`'s `findPoisNear` with `ensureFromLocation`
|
|
629
|
+
* so every returned POI is either reused from the local DB (when the
|
|
630
|
+
* provider returns an id matching an existing Place `externalId`) or
|
|
631
|
+
* created fresh otherwise. The cache is effectively automatic because
|
|
632
|
+
* the provider's own place_id becomes the Place row's `externalId`, so
|
|
633
|
+
* calling `discoverNearby` twice for the same area on the same provider
|
|
634
|
+
* is a no-op after the first run when the provider returns stable ids.
|
|
635
|
+
*
|
|
636
|
+
* Requires a geo provider that implements `findPoisNear`. Throws a clear
|
|
637
|
+
* error otherwise so consumers can fall back to `lookupOrCreate` or
|
|
638
|
+
* switch providers.
|
|
639
|
+
*/
|
|
640
|
+
async discoverNearby(latitude, longitude, radiusMeters, options = {}) {
|
|
641
|
+
if (!(radiusMeters > 0)) {
|
|
642
|
+
throw new Error(
|
|
643
|
+
`discoverNearby: radiusMeters must be > 0 (got ${radiusMeters})`
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
const detailed = await this.discoverNearbyDetailed(
|
|
647
|
+
latitude,
|
|
648
|
+
longitude,
|
|
649
|
+
radiusMeters,
|
|
650
|
+
options
|
|
651
|
+
);
|
|
652
|
+
return detailed.map((d) => d.place);
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Internal variant of `discoverNearby` that exposes whether each
|
|
656
|
+
* returned Place was created during this call (`true`) or reused from
|
|
657
|
+
* an earlier call (`false`). Used by `resolveTrackPlaces` to classify
|
|
658
|
+
* buckets as cache hits without having to re-scan the Place table.
|
|
659
|
+
*/
|
|
660
|
+
async discoverNearbyDetailed(latitude, longitude, radiusMeters, options = {}) {
|
|
661
|
+
const {
|
|
662
|
+
geoProvider = "openstreetmap",
|
|
663
|
+
types,
|
|
664
|
+
keyword,
|
|
665
|
+
limit,
|
|
666
|
+
language,
|
|
667
|
+
typeSlug,
|
|
668
|
+
parentId
|
|
669
|
+
} = options;
|
|
670
|
+
const geo = await this.getGeoAdapter(geoProvider);
|
|
671
|
+
if (typeof geo.findPoisNear !== "function") {
|
|
672
|
+
throw new Error(
|
|
673
|
+
`Geo provider '${geoProvider}' does not implement findPoisNear`
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
const results = await geo.findPoisNear(latitude, longitude, radiusMeters, {
|
|
677
|
+
types,
|
|
678
|
+
keyword,
|
|
679
|
+
limit,
|
|
680
|
+
language
|
|
681
|
+
});
|
|
682
|
+
const detailed = [];
|
|
683
|
+
for (const result of results) {
|
|
684
|
+
detailed.push(await this.ensureFromLocation(result, typeSlug, parentId));
|
|
685
|
+
}
|
|
686
|
+
return detailed;
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Resolve POIs along a GPS track (e.g. a video's per-frame path).
|
|
690
|
+
*
|
|
691
|
+
* Naively walking every point would hammer the provider with redundant
|
|
692
|
+
* requests — consecutive samples are usually within a few meters. This
|
|
693
|
+
* method buckets points into a `bucketMeters`-wide grid, calls
|
|
694
|
+
* `discoverNearby` once per distinct bucket, and throttles requests per
|
|
695
|
+
* `throttleMs` so free tiers (Overpass, Nominatim) stay inside their
|
|
696
|
+
* community rate limits without the caller having to manage a queue.
|
|
697
|
+
*
|
|
698
|
+
* The returned `places` are deduped across buckets by Place id, so a
|
|
699
|
+
* POI that falls within several overlapping search radii appears once.
|
|
700
|
+
*/
|
|
701
|
+
async resolveTrackPlaces(points, options = {}) {
|
|
702
|
+
const radiusMeters = options.radiusMeters ?? 50;
|
|
703
|
+
const bucketMeters = options.bucketMeters ?? 50;
|
|
704
|
+
const throttleMs = options.throttleMs ?? 1100;
|
|
705
|
+
if (!(radiusMeters > 0)) {
|
|
706
|
+
throw new Error(
|
|
707
|
+
`resolveTrackPlaces: radiusMeters must be > 0 (got ${radiusMeters})`
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
if (!(bucketMeters > 0)) {
|
|
711
|
+
throw new Error(
|
|
712
|
+
`resolveTrackPlaces: bucketMeters must be > 0 (got ${bucketMeters})`
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
if (throttleMs < 0 || !Number.isFinite(throttleMs)) {
|
|
716
|
+
throw new Error(
|
|
717
|
+
`resolveTrackPlaces: throttleMs must be a finite non-negative number (got ${throttleMs})`
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
const bucketMetersKm = bucketMeters / 1e3;
|
|
721
|
+
const bucketCenters = [];
|
|
722
|
+
for (const point of points) {
|
|
723
|
+
if (!Number.isFinite(point.lat) || !Number.isFinite(point.lng)) continue;
|
|
724
|
+
let bestIdx = -1;
|
|
725
|
+
let bestKm = Number.POSITIVE_INFINITY;
|
|
726
|
+
for (let i = 0; i < bucketCenters.length; i += 1) {
|
|
727
|
+
const c = bucketCenters[i];
|
|
728
|
+
const km = this.calculateDistance(c.lat, c.lng, point.lat, point.lng);
|
|
729
|
+
if (km <= bucketMetersKm && km < bestKm) {
|
|
730
|
+
bestKm = km;
|
|
731
|
+
bestIdx = i;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
if (bestIdx < 0) bucketCenters.push(point);
|
|
735
|
+
}
|
|
736
|
+
const result = {
|
|
737
|
+
places: [],
|
|
738
|
+
requestCount: 0,
|
|
739
|
+
cacheHitCount: 0,
|
|
740
|
+
bucketCount: bucketCenters.length
|
|
741
|
+
};
|
|
742
|
+
const seen = /* @__PURE__ */ new Map();
|
|
743
|
+
let lastCallAt = 0;
|
|
744
|
+
for (const center of bucketCenters) {
|
|
745
|
+
const wait = lastCallAt + throttleMs - Date.now();
|
|
746
|
+
if (wait > 0) {
|
|
747
|
+
await new Promise((resolve) => setTimeout(resolve, wait));
|
|
748
|
+
}
|
|
749
|
+
const detailed = await this.discoverNearbyDetailed(
|
|
750
|
+
center.lat,
|
|
751
|
+
center.lng,
|
|
752
|
+
radiusMeters,
|
|
753
|
+
options
|
|
754
|
+
);
|
|
755
|
+
lastCallAt = Date.now();
|
|
756
|
+
result.requestCount += 1;
|
|
757
|
+
if (detailed.length > 0 && detailed.every((d) => !d.created)) {
|
|
758
|
+
result.cacheHitCount += 1;
|
|
759
|
+
}
|
|
760
|
+
for (const { place } of detailed) {
|
|
761
|
+
if (place.id && !seen.has(place.id)) seen.set(place.id, place);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
result.places = [...seen.values()];
|
|
765
|
+
return result;
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Provider wiring (env keys, user-agent string, etc.) that `geocode`
|
|
769
|
+
* and `discoverNearby` share so the two code paths can't drift. Kept
|
|
770
|
+
* separate from the adapter call itself so tests and future providers
|
|
771
|
+
* can inspect or override the options before construction.
|
|
772
|
+
*/
|
|
773
|
+
getGeoAdapterOptions(provider) {
|
|
774
|
+
if (provider === "google") {
|
|
775
|
+
return {
|
|
776
|
+
provider: "google",
|
|
777
|
+
apiKey: process.env.GOOGLE_MAPS_API_KEY || ""
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
return { provider: "openstreetmap", userAgent: "@have/places" };
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Build a geo adapter configured for this call. Single source of truth
|
|
784
|
+
* for provider wiring — `geocode` and `discoverNearby` both come through
|
|
785
|
+
* here.
|
|
786
|
+
*/
|
|
787
|
+
async getGeoAdapter(provider) {
|
|
788
|
+
return getGeoAdapter(this.getGeoAdapterOptions(provider));
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Get immediate children of a parent place
|
|
792
|
+
*
|
|
793
|
+
* @param parentId - The parent place ID
|
|
794
|
+
* @returns Array of child places
|
|
795
|
+
*/
|
|
796
|
+
async getChildren(parentId) {
|
|
797
|
+
return await this.list({
|
|
798
|
+
where: { parentId }
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Get root places (no parent)
|
|
803
|
+
*
|
|
804
|
+
* @returns Array of root places
|
|
805
|
+
*/
|
|
806
|
+
async getRootPlaces() {
|
|
807
|
+
return await this.list({
|
|
808
|
+
where: { parentId: null }
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Get places by type
|
|
813
|
+
*
|
|
814
|
+
* @param typeSlug - PlaceType slug
|
|
815
|
+
* @returns Array of places of that type
|
|
816
|
+
*/
|
|
817
|
+
async getByType(typeSlug) {
|
|
818
|
+
const typeCollection = await PlaceTypeCollection.create(
|
|
819
|
+
this.options
|
|
820
|
+
);
|
|
821
|
+
const placeType = await typeCollection.getBySlug(typeSlug);
|
|
822
|
+
if (!placeType) return [];
|
|
823
|
+
return await this.list({
|
|
824
|
+
where: { typeId: placeType.id }
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Get place hierarchy (all ancestors and descendants)
|
|
829
|
+
*
|
|
830
|
+
* @param placeId - The place ID
|
|
831
|
+
* @returns Object with ancestors, current place, and descendants
|
|
832
|
+
*/
|
|
833
|
+
async getHierarchy(placeId) {
|
|
834
|
+
const place = await this.get({ id: placeId });
|
|
835
|
+
if (!place) throw new Error(`Place '${placeId}' not found`);
|
|
836
|
+
return await place.getHierarchy();
|
|
837
|
+
}
|
|
838
|
+
async getAssets(placeId, relationship) {
|
|
839
|
+
return getOwnedAssetsFromCollection(this, placeId, relationship);
|
|
840
|
+
}
|
|
841
|
+
async addAsset(placeId, asset, relationship = "attachment", sortOrder = 0) {
|
|
842
|
+
await addOwnedAssetFromCollection(
|
|
843
|
+
this,
|
|
844
|
+
"Place",
|
|
845
|
+
placeId,
|
|
846
|
+
asset,
|
|
847
|
+
relationship,
|
|
848
|
+
sortOrder
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
async removeAsset(placeId, assetId, relationship) {
|
|
852
|
+
await removeOwnedAssetFromCollection(
|
|
853
|
+
this,
|
|
854
|
+
"Place",
|
|
855
|
+
placeId,
|
|
856
|
+
assetId,
|
|
857
|
+
relationship
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Search places by proximity to coordinates
|
|
862
|
+
*
|
|
863
|
+
* @param latitude - Center latitude
|
|
864
|
+
* @param longitude - Center longitude
|
|
865
|
+
* @param radiusKm - Search radius in kilometers
|
|
866
|
+
* @returns Array of places within radius, sorted by distance
|
|
867
|
+
*/
|
|
868
|
+
async searchByProximity(latitude, longitude, radiusKm = 10) {
|
|
869
|
+
const places = await this.list({
|
|
870
|
+
where: {
|
|
871
|
+
latitude: { $ne: null },
|
|
872
|
+
longitude: { $ne: null }
|
|
873
|
+
}
|
|
874
|
+
});
|
|
875
|
+
const placesWithDistance = places.map((place) => {
|
|
876
|
+
if (place.latitude === null || place.longitude === null) return null;
|
|
877
|
+
const distance = this.calculateDistance(
|
|
878
|
+
latitude,
|
|
879
|
+
longitude,
|
|
880
|
+
place.latitude,
|
|
881
|
+
place.longitude
|
|
882
|
+
);
|
|
883
|
+
return { place, distance };
|
|
884
|
+
}).filter(
|
|
885
|
+
(p) => p !== null && p.distance <= radiusKm
|
|
886
|
+
).sort((a, b) => a.distance - b.distance);
|
|
887
|
+
return placesWithDistance.map((p) => p.place);
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Calculate distance between two coordinates using Haversine formula
|
|
891
|
+
*
|
|
892
|
+
* @param lat1 - First latitude
|
|
893
|
+
* @param lng1 - First longitude
|
|
894
|
+
* @param lat2 - Second latitude
|
|
895
|
+
* @param lng2 - Second longitude
|
|
896
|
+
* @returns Distance in kilometers
|
|
897
|
+
*/
|
|
898
|
+
calculateDistance(lat1, lng1, lat2, lng2) {
|
|
899
|
+
const R = 6371;
|
|
900
|
+
const dLat = this.toRad(lat2 - lat1);
|
|
901
|
+
const dLng = this.toRad(lng2 - lng1);
|
|
902
|
+
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(this.toRad(lat1)) * Math.cos(this.toRad(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2);
|
|
903
|
+
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
904
|
+
return R * c;
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Convert degrees to radians
|
|
908
|
+
*/
|
|
909
|
+
toRad(degrees) {
|
|
910
|
+
return degrees * (Math.PI / 180);
|
|
911
|
+
}
|
|
912
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
913
|
+
// Tenant Helper Methods
|
|
914
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
915
|
+
/**
|
|
916
|
+
* Find all places belonging to a specific tenant
|
|
917
|
+
*
|
|
918
|
+
* @param tenantId - The tenant ID to filter by
|
|
919
|
+
* @returns Array of places for the tenant
|
|
920
|
+
*/
|
|
921
|
+
async findByTenant(tenantId2) {
|
|
922
|
+
return this.list({ where: { tenantId: tenantId2 } });
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* Find all global places (not associated with any tenant)
|
|
926
|
+
*
|
|
927
|
+
* @returns Array of global places
|
|
928
|
+
*/
|
|
929
|
+
async findGlobal() {
|
|
930
|
+
return this.list({ where: { tenantId: null } });
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Find places for a tenant including global places
|
|
934
|
+
*
|
|
935
|
+
* @param tenantId - The tenant ID to include
|
|
936
|
+
* @returns Array of tenant-specific and global places
|
|
937
|
+
*/
|
|
938
|
+
async findWithGlobals(tenantId2) {
|
|
939
|
+
return this.query(
|
|
940
|
+
`SELECT * FROM ${this.tableName} WHERE tenant_id = ? OR tenant_id IS NULL`,
|
|
941
|
+
[tenantId2]
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
const PlaceCollection$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
946
|
+
__proto__: null,
|
|
947
|
+
PlaceCollection
|
|
948
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
949
|
+
export {
|
|
950
|
+
Place,
|
|
951
|
+
PlaceAsset,
|
|
952
|
+
PlaceAssetCollection,
|
|
953
|
+
PlaceCollection,
|
|
954
|
+
PlaceType,
|
|
955
|
+
PlaceTypeCollection,
|
|
956
|
+
areCoordinatesNear,
|
|
957
|
+
calculateDistance,
|
|
958
|
+
formatCoordinates,
|
|
959
|
+
generateDisplayName,
|
|
960
|
+
locationToGeoData,
|
|
961
|
+
mapLocationTypeToPlaceType,
|
|
962
|
+
normalizeAddressComponents,
|
|
963
|
+
parseCoordinates,
|
|
964
|
+
validateCoordinates
|
|
965
|
+
};
|
|
966
|
+
//# sourceMappingURL=index.js.map
|