Логика геозоны оптимизирована или нет для данных о местоположении GPS длиной 1000 или что-то в этом роде для центра журнJavascript

Форум по Javascript
Ответить
Anonymous
 Логика геозоны оптимизирована или нет для данных о местоположении GPS длиной 1000 или что-то в этом роде для центра журн

Сообщение Anonymous »

Я получаю данные о местоположении с устройства GPS и извлекаю координаты широты и долготы геозоны из своей базы данных. Я также проверяю, какая геозона связана с каким ребенком, и получаю токены FCM родителей. Если точка геозоны и точка положения совпадают в заданном радиусе, я отправляю уведомление родителям. Правильный ли это способ сделать это и оптимизирован ли он для обработки около 2000 точек данных GPS или геозон? Я получаю данные GPS каждые 5 секунд.

const NOTIFY_THROTTLE_MS = 60 * 1000;
const CACHE_REFRESH_INTERVAL_MS = 60 * 1000;
const POSITION_STALE_MS = 30 * 1000;
const NOTIFY_CONCURRENCY = 8;

const childrenById = new Map();
const childrenByBus = new Map();
const lastGeoStatus = new Map();
const tripModeByDevice = new Map();

let lastCacheUpdate = new Date(0);

const notifyQueue = [];
let runningNotify = 0;

function enqueueNotify(job) {
notifyQueue.push(job);
processNotifyQueue();
}
function processNotifyQueue() {
while (runningNotify < NOTIFY_CONCURRENCY && notifyQueue.length) {
const job = notifyQueue.shift();
runningNotify++;
job().finally(() => {
runningNotify--;
// yield
setImmediate(processNotifyQueue);
});
}
}

// >>>>>>>>>>>>>>>>>>>>>>>>>>>correction start

export function buildGeoMeta(geo) {
if (!geo) return null;

// Case 1: When structure is like geo.area.center = [lat, lng]
if (geo.area && Array.isArray(geo.area.center) && typeof geo.area.radius === "number") {
const [lat, lng] = geo.area.center;
const r = geo.area.radius; // meters

// Approximate deltas for bounding box
const dLat = r / 111320;
const dLng = r / (111320 * Math.cos((lat * Math.PI) / 180) || 1);

return {
type: "circle",
center: { lat, lng },
radius: r,
bbox: {
minLat: lat - dLat,
maxLat: lat + dLat,
minLng: lng - dLng,
maxLng: lng + dLng,
},
};
}

// Case 2: Polygon-type geofence
if (geo.area && Array.isArray(geo.area.coordinates)) {
const coords = geo.area.coordinates.map((p) =>
Array.isArray(p)
? { lat: p[1], lng: p[0] }
: { lat: p.lat, lng: p.lng }
);

let minLat = Infinity,
maxLat = -Infinity,
minLng = Infinity,
maxLng = -Infinity;

coords.forEach((c) => {
minLat = Math.min(minLat, c.lat);
maxLat = Math.max(maxLat, c.lat);
minLng = Math.min(minLng, c.lng);
maxLng = Math.max(maxLng, c.lng);
});

return {
type: "polygon",
coords,
bbox: { minLat, maxLat, minLng, maxLng },
};
}

// Case 3: Old structure (backward compatibility)
if (geo.center && typeof geo.radius === "number") {
const lat = geo.center.lat;
const lng = geo.center.lng;
const r = geo.radius;

const dLat = r / 111320;
const dLng = r / (111320 * Math.cos((lat * Math.PI) / 180) || 1);

return {
type: "circle",
center: { lat, lng },
radius: r,
bbox: {
minLat: lat - dLat,
maxLat: lat + dLat,
minLng: lng - dLng,
maxLng: lng + dLng,
},
};
}

// Fallback
return null;
}

export async function refreshChildrenCacheOnce() {
try {
if (!lastCacheUpdate) {
// console.log("[Geofence] No previous cache update timestamp found, performing full reload...");
return await fullChildrenCacheReload();
}

const changed = await Child.find({
updatedAt: { $gt: lastCacheUpdate },
$or: [
{ pickupGeoId: { $exists: true, $ne: null } },
{ dropGeoId: { $exists: true, $ne: null } },
],
})
.populate({
path: "pickupGeoId",
select: "geofenceName area",
})
.populate({
path: "dropGeoId",
select: "geofenceName area",
})
.populate({
path: "parentId",
select: "fcmToken",
}).lean();

// console.log(`[Geofence] Found ${changed.length} updated children since last cache refresh`);

for (const c of changed) {
const id = String(c._id);
const imei = c.routeObjId?.deviceObjId?.uniqueId;

if (!imei) continue; // Skip if no device linked

const childData = {
childId: id,
childName: c.childName,
imei,
parentFcmToken: c.parentId?.fcmToken || [],
pickupGeoRaw: c.pickupGeoId,
dropGeoRaw: c.dropGeoId,
pickupGeo: buildGeoMeta(c.pickupGeoId),
dropGeo: buildGeoMeta(c.dropGeoId),
pickupTime:c.pickupTime,
dropTime:c.dropTime
};

childrenById.set(id, childData);

const busArr = childrenByBus.get(imei) || [];
const existsIndex = busArr.findIndex((x) => x.childId === id);
if (existsIndex >= 0) busArr[existsIndex] = childData;
else busArr.push(childData);
childrenByBus.set(imei, busArr);

if (!lastGeoStatus.has(id)) {
lastGeoStatus.set(id, {
pickup: "outside",
drop: "outside",
lastNotified: { pickup: 0, drop: 0 },
lastSeenTs: 0,
});
}
}

lastCacheUpdate = new Date();
// console.log(`[Geofence] Cache refresh applied successfully: ${changed.length} children updated`);

} catch (err) {
console.error("[Geofence] refreshChildrenCacheOnce error:", err);
}
}

export async function fullChildrenCacheReload() {
try {
const activeBranches = await Branch.find({
"notificationsEnabled.geofence": true,
}).select('_id');

const all = await Child.find({
branchId: { $in: activeBranches.map(b => b._id) },
$or: [
{ pickupGeoId: { $exists: true, $ne: null } },
{ dropGeoId: { $exists: true, $ne: null } },
],
})
.populate({
path: "branchId",
match: { "notificationsEnabled.geofence": true },
})
.populate({
path: "routeObjId",
populate: {
path: "deviceObjId",
select: "uniqueId",
},
})
.populate({
path: "pickupGeoId",
select: "geofenceName area",
})
.populate({
path: "dropGeoId",
select: "geofenceName area",
})
.populate({
path: "parentId",
select: "fcmToken",
}).lean();

console.log(`[Geofence] Total fetched children: ${all.length}`);

childrenById.clear();
childrenByBus.clear();
lastGeoStatus.clear();

for (const c of all) {
const id = String(c._id);
const imei = c.routeObjId?.deviceObjId?.uniqueId;

// Skip if no linked device or invalid IMEI
if (!imei) continue;

const childData = {
childId: id,
childName: c.childName,
imei,
parentFcmToken: c.parentId?.fcmToken || [],
pickupGeoRaw: c.pickupGeoId,
dropGeoRaw: c.dropGeoId,
pickupGeo: buildGeoMeta(c.pickupGeoId),
dropGeo: buildGeoMeta(c.dropGeoId),
pickupTime:c.pickupTime,
dropTime:c.dropTime,
};

// Store child data by ID
childrenById.set(id, childData);

// Group by IMEI (device)
const arr = childrenByBus.get(imei) || [];
arr.push(childData);
childrenByBus.set(imei, arr);

// Initialize last known status
lastGeoStatus.set(id, {
pickup: "outside",
drop: "outside",
lastNotified: { pickup: 0, drop: 0 },
lastSeenTs: 0,
});
}

lastCacheUpdate = new Date();
// console.log(`[Geofence] Full cache reloaded: ${childrenById.size} active children`);

} catch (err) {
console.error("[Geofence] fullChildrenCacheReload error:", err);
}
}

function isPointInCircleBBox(lat, lng, geo) {
const b = geo.bbox;
return lat >= b.minLat && lat = b.minLng && lng >>>>>>>>>>>>>>>>>>>>>>>>>>correction end

function shouldThrottleNotification(childId, type) {
// console.log("aaaaaaaaaaaaaaaaa",childId,type)
const st = lastGeoStatus.get(childId);
if (!st) return false;
const last = st.lastNotified[type] || 0;
return Date.now() - last < NOTIFY_THROTTLE_MS;
}

function markNotified(childId, type) {
const st = lastGeoStatus.get(childId) || {};
st.lastNotified = st.lastNotified || {};
st.lastNotified[type] = Date.now();
lastGeoStatus.set(childId, st);
}

async function sendNotificationSafe(fcmTokens, title, body) {

try {
if (!fcmTokens) return;

// Ensure we have an array
const tokensArray = Array.isArray(fcmTokens) ? fcmTokens : [fcmTokens];
// console.log(tokensArray,"sssssssssssssssssssss")

// Send notification to each token
for (const token of tokensArray) {
if (token) {
// console.log("Sending to:", token, title, body);
await sendFirebaseNotification({
token,
title,
body,
data: { customKey: 'customValue' }, // optional
});
}
}

} catch (err) {
console.error("[Geofence] sendFirebaseNotification failed:", err?.message || err);
}
}

// >>>>>>>>>>>>>> main working function start

export function processDevicePosition(imei, device) {
if (!device || typeof device.latitude !== "number" || typeof device.longitude !== "number") return;

const children = childrenByBus.get(String(imei));
if (!children || children.length === 0) return;

const now = new Date();

for (const child of children) {
const childId = child.childId;

const prev = lastGeoStatus.get(childId) || {
pickup: "outside",
drop: "outside",
lastNotified: { pickup: 0, drop: 0 },
lastSeenTs: 0,
};

const next = { ...prev };

const parseTime = (timeStr) => {
if (!timeStr) return null;
const [time, modifier] = timeStr.split(" ");
if (!time || !modifier) return null;
let [hours, minutes] = time.split(":").map(Number);
if (modifier.toLowerCase() === "pm" && hours < 12) hours += 12;
if (modifier.toLowerCase() === "am" && hours === 12) hours = 0;
const date = new Date();
date.setHours(hours, minutes || 0, 0, 0);
return date;
};

const pickupTime = typeof child.pickupTime === "string" ? parseTime(child.pickupTime) : new Date(child.pickupTime);
const dropTime = typeof child.dropTime === "string" ? parseTime(child.dropTime) : new Date(child.dropTime);

// Determine which geofence (pickup or drop) is more relevant now
let nearestType = null;
if (pickupTime && dropTime) {
const diffPickup = Math.abs(now - pickupTime);
const diffDrop = Math.abs(now - dropTime);
nearestType = diffPickup

Подробнее здесь: https://stackoverflow.com/questions/798 ... -or-someth
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «Javascript»