| Line 125... |
Line 125... |
| 125 |
* Math.sin(dLng / 2) * Math.sin(dLng / 2);
|
125 |
* Math.sin(dLng / 2) * Math.sin(dLng / 2);
|
| 126 |
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
126 |
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
| 127 |
return R * c;
|
127 |
return R * c;
|
| 128 |
}
|
128 |
}
|
| 129 |
|
129 |
|
| - |
|
130 |
// Reorder a day's bulk-upload stops in place into a greedy nearest-neighbor
|
| - |
|
131 |
// route from the given start point (the beat's start location). Codes with no
|
| - |
|
132 |
// known coordinates (offices, or partners missing lat/lng) are parked at the
|
| - |
|
133 |
// end preserving their relative order rather than dropped. Sequence is taken
|
| - |
|
134 |
// from the resulting list position by the caller.
|
| - |
|
135 |
private static void sortPartnersByNearestNeighbor(List<BulkPartner> partners, Double startLat, Double startLng, Map<String, double[]> codeToLatLng) {
|
| - |
|
136 |
if (partners == null || partners.size() < 2) return;
|
| - |
|
137 |
|
| - |
|
138 |
List<BulkPartner> routable = new ArrayList<>();
|
| - |
|
139 |
List<BulkPartner> parked = new ArrayList<>();
|
| - |
|
140 |
for (BulkPartner p : partners) {
|
| - |
|
141 |
if (codeToLatLng.containsKey(p.code)) routable.add(p);
|
| - |
|
142 |
else parked.add(p);
|
| - |
|
143 |
}
|
| - |
|
144 |
|
| - |
|
145 |
List<BulkPartner> ordered = new ArrayList<>();
|
| - |
|
146 |
double curLat, curLng;
|
| - |
|
147 |
if (startLat != null && startLng != null) {
|
| - |
|
148 |
curLat = startLat;
|
| - |
|
149 |
curLng = startLng;
|
| - |
|
150 |
} else if (!routable.isEmpty()) {
|
| - |
|
151 |
BulkPartner first = routable.remove(0);
|
| - |
|
152 |
ordered.add(first);
|
| - |
|
153 |
double[] c = codeToLatLng.get(first.code);
|
| - |
|
154 |
curLat = c[0];
|
| - |
|
155 |
curLng = c[1];
|
| - |
|
156 |
} else {
|
| - |
|
157 |
return;
|
| - |
|
158 |
}
|
| - |
|
159 |
|
| - |
|
160 |
while (!routable.isEmpty()) {
|
| - |
|
161 |
int bestIdx = 0;
|
| - |
|
162 |
double bestD = Double.MAX_VALUE;
|
| - |
|
163 |
for (int i = 0; i < routable.size(); i++) {
|
| - |
|
164 |
double[] c = codeToLatLng.get(routable.get(i).code);
|
| - |
|
165 |
double d = haversineKm(curLat, curLng, c[0], c[1]);
|
| - |
|
166 |
if (d < bestD) {
|
| - |
|
167 |
bestD = d;
|
| - |
|
168 |
bestIdx = i;
|
| - |
|
169 |
}
|
| - |
|
170 |
}
|
| - |
|
171 |
BulkPartner next = routable.remove(bestIdx);
|
| - |
|
172 |
ordered.add(next);
|
| - |
|
173 |
double[] c = codeToLatLng.get(next.code);
|
| - |
|
174 |
curLat = c[0];
|
| - |
|
175 |
curLng = c[1];
|
| - |
|
176 |
}
|
| - |
|
177 |
|
| - |
|
178 |
partners.clear();
|
| - |
|
179 |
partners.addAll(ordered);
|
| - |
|
180 |
partners.addAll(parked);
|
| - |
|
181 |
}
|
| - |
|
182 |
|
| 130 |
// Mirrors the JS recalcDay() formula. Used by schedule/repeat endpoints
|
183 |
// Mirrors the JS recalcDay() formula. Used by schedule/repeat endpoints
|
| 131 |
// which create fresh BeatSchedule rows — they need to fill totals from the
|
184 |
// which create fresh BeatSchedule rows — they need to fill totals from the
|
| 132 |
// existing beat_route table, not from anything the client posted.
|
185 |
// existing beat_route table, not from anything the client posted.
|
| 133 |
// Returns {totalKm, totalMins}.
|
186 |
// Returns {totalKm, totalMins}.
|
| 134 |
private double[] computeDayTotals(int beatId, int dayNumber, String endAction) {
|
187 |
private double[] computeDayTotals(int beatId, int dayNumber, String endAction) {
|
| Line 2231... |
Line 2284... |
| 2231 |
workbook.close();
|
2284 |
workbook.close();
|
| 2232 |
|
2285 |
|
| 2233 |
// Partner-code lookup (legacy).
|
2286 |
// Partner-code lookup (legacy).
|
| 2234 |
List<FofoStore> allStores = fofoStoreRepository.selectAll();
|
2287 |
List<FofoStore> allStores = fofoStoreRepository.selectAll();
|
| 2235 |
Map<String, Integer> codeToId = new HashMap<>();
|
2288 |
Map<String, Integer> codeToId = new HashMap<>();
|
| - |
|
2289 |
Map<String, double[]> codeToLatLng = new HashMap<>();
|
| - |
|
2290 |
for (FofoStore store : allStores) {
|
| 2236 |
for (FofoStore store : allStores) codeToId.put(store.getCode(), store.getId());
|
2291 |
codeToId.put(store.getCode(), store.getId());
|
| - |
|
2292 |
Double la = parseDoubleOrNull(store.getLatitude());
|
| - |
|
2293 |
Double lo = parseDoubleOrNull(store.getLongitude());
|
| - |
|
2294 |
if (la != null && lo != null) codeToLatLng.put(store.getCode(), new double[]{la, lo});
|
| - |
|
2295 |
}
|
| 2237 |
|
2296 |
|
| 2238 |
// Office-code lookup — office stops share the same `partner_code` column in the bulk
|
2297 |
// Office-code lookup — office stops share the same `partner_code` column in the bulk
|
| 2239 |
// sheet; resolution dispatches by which catalogue the code belongs to. A code present
|
2298 |
// sheet; resolution dispatches by which catalogue the code belongs to. A code present
|
| 2240 |
// in BOTH catalogues is treated as an error so the planner fixes the collision.
|
2299 |
// in BOTH catalogues is treated as an error so the planner fixes the collision.
|
| 2241 |
Map<String, Integer> officeCodeToId = new HashMap<>();
|
2300 |
Map<String, Integer> officeCodeToId = new HashMap<>();
|
| Line 2402... |
Line 2461... |
| 2402 |
schedule.setEndAction(dayIdx == sortedDays.size() - 1 ? "HOME" : "DAYBREAK");
|
2461 |
schedule.setEndAction(dayIdx == sortedDays.size() - 1 ? "HOME" : "DAYBREAK");
|
| 2403 |
schedule.setCreatedTimestamp(LocalDateTime.now());
|
2462 |
schedule.setCreatedTimestamp(LocalDateTime.now());
|
| 2404 |
beatScheduleRepository.persist(schedule);
|
2463 |
beatScheduleRepository.persist(schedule);
|
| 2405 |
|
2464 |
|
| 2406 |
List<BulkPartner> partners = g.dayToPartners.get(dayNumber);
|
2465 |
List<BulkPartner> partners = g.dayToPartners.get(dayNumber);
|
| 2407 |
partners.sort((a, b) -> {
|
2466 |
// Auto-order the day into a nearest-neighbor route from the beat's
|
| 2408 |
if (a.seq >= 0 && b.seq >= 0) return Integer.compare(a.seq, b.seq);
|
2467 |
// start location, matching the interactive planner. The sheet's
|
| - |
|
2468 |
// sequence_order column is intentionally ignored.
|
| 2409 |
if (a.seq >= 0) return -1;
|
2469 |
sortPartnersByNearestNeighbor(partners,
|
| 2410 |
if (b.seq >= 0) return 1;
|
2470 |
parseDoubleOrNull(beat.getStartLatitude()),
|
| 2411 |
return Integer.compare(a.rowNum, b.rowNum);
|
2471 |
parseDoubleOrNull(beat.getStartLongitude()),
|
| 2412 |
});
|
2472 |
codeToLatLng);
|
| 2413 |
|
2473 |
|
| 2414 |
int autoSeq = 0;
|
2474 |
int autoSeq = 0;
|
| 2415 |
for (BulkPartner p : partners) {
|
2475 |
for (BulkPartner p : partners) {
|
| 2416 |
Integer partnerId = codeToId.get(p.code);
|
2476 |
Integer partnerId = codeToId.get(p.code);
|
| 2417 |
Integer officeId = officeCodeToId.get(p.code);
|
2477 |
Integer officeId = officeCodeToId.get(p.code);
|
| Line 2424... |
Line 2484... |
| 2424 |
route.setVisitType(com.spice.profitmandi.dao.enumuration.dtr.BeatVisitType.PARTNER);
|
2484 |
route.setVisitType(com.spice.profitmandi.dao.enumuration.dtr.BeatVisitType.PARTNER);
|
| 2425 |
} else {
|
2485 |
} else {
|
| 2426 |
route.setFofoId(officeId);
|
2486 |
route.setFofoId(officeId);
|
| 2427 |
route.setVisitType(com.spice.profitmandi.dao.enumuration.dtr.BeatVisitType.OFFICE);
|
2487 |
route.setVisitType(com.spice.profitmandi.dao.enumuration.dtr.BeatVisitType.OFFICE);
|
| 2428 |
}
|
2488 |
}
|
| - |
|
2489 |
// Sequence follows the nearest-neighbor ordering above, not the sheet.
|
| 2429 |
route.setSequenceOrder(p.seq >= 0 ? p.seq : autoSeq);
|
2490 |
route.setSequenceOrder(autoSeq);
|
| 2430 |
route.setDayNumber(dayNumber);
|
2491 |
route.setDayNumber(dayNumber);
|
| 2431 |
route.setActive(true);
|
2492 |
route.setActive(true);
|
| 2432 |
beatRouteRepository.persist(route);
|
2493 |
beatRouteRepository.persist(route);
|
| 2433 |
autoSeq++;
|
2494 |
autoSeq++;
|
| 2434 |
}
|
2495 |
}
|