72 lines
3.5 KiB
Go
72 lines
3.5 KiB
Go
// Package legs answers one question, in one place: does a parcel go straight to
|
|
// its receiver, or does it go via a base first?
|
|
//
|
|
// It exists because two packages need that answer and neither can import the
|
|
// other. controllers decides it at pickup-complete (which state the consignment
|
|
// lands in, and which base the rider is sent to); internal/routing needs it when
|
|
// it sequences a rider's stops, because a hub-routed parcel's next stop is the
|
|
// BASE, not the address on the booking. controllers already imports
|
|
// internal/routing, so routing cannot import back — and a second copy of the
|
|
// rule in routing is exactly the kind of duplication this codebase has been
|
|
// bitten by before (the rival status table in dispatchShared, the batch windows
|
|
// that drifted between two pages).
|
|
//
|
|
// So the rule lives here and both call it. Neither owns it.
|
|
package legs
|
|
|
|
import "math"
|
|
|
|
// HaversineKM is the straight-line distance between two points, in kilometres.
|
|
// One definition for the whole codebase — controllers.haversineKM delegates to
|
|
// it rather than keeping a second copy.
|
|
func HaversineKM(lat1, lon1, lat2, lon2 float64) float64 {
|
|
const earthRadiusKM = 6371.0
|
|
toRad := func(deg float64) float64 { return deg * math.Pi / 180 }
|
|
dLat := toRad(lat2 - lat1)
|
|
dLon := toRad(lon2 - lon1)
|
|
a := math.Sin(dLat/2)*math.Sin(dLat/2) +
|
|
math.Cos(toRad(lat1))*math.Cos(toRad(lat2))*math.Sin(dLon/2)*math.Sin(dLon/2)
|
|
return earthRadiusKM * 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
|
|
}
|
|
|
|
// MaxHyperlocalKM bounds the straight-line pickup→delivery distance under which
|
|
// a booking with a missing or unusable pincode is still treated as hyperlocal.
|
|
// It is ONLY consulted when the pincode rule cannot decide: console-created
|
|
// kitchen→customer bookings frequently carry accurate coordinates and no
|
|
// delivery pincode, and must not be routed through a base because of it.
|
|
const MaxHyperlocalKM = 30.0
|
|
|
|
// SamePostalArea reports whether two pincodes fall in the same 3-digit postal
|
|
// area. Pincodes shorter than 3 characters are treated as unknown rather than
|
|
// matching, so bad data falls back to the safe hub route instead of quietly
|
|
// claiming two parcels belong together.
|
|
func SamePostalArea(pickupPincode, deliveryPincode string) bool {
|
|
if len(pickupPincode) < 3 || len(deliveryPincode) < 3 {
|
|
return false
|
|
}
|
|
return pickupPincode[:3] == deliveryPincode[:3]
|
|
}
|
|
|
|
// IsHyperlocal decides whether a parcel can skip the base and be carried
|
|
// straight to the receiver by the collecting rider.
|
|
//
|
|
// The pincode-prefix rule decides outright when both pincodes are present — a
|
|
// matching prefix is hyperlocal, a differing one is genuinely inter-area, and
|
|
// distance does not get a vote. Pollachi is 40km from Coimbatore, closer than
|
|
// plenty of runs treated as local, and it is still a different postal area.
|
|
//
|
|
// Only when a pincode is missing or too short does the straight-line distance
|
|
// decide instead. With neither pincodes nor coordinates the answer is false,
|
|
// which routes via a base — the safe direction to be wrong in, because a parcel
|
|
// that reaches a base can still be forwarded, while one handed to a rider who
|
|
// cannot reach the receiver is stuck.
|
|
func IsHyperlocal(pickupPincode, deliveryPincode string, pLat, pLng, dLat, dLng float64) bool {
|
|
if len(pickupPincode) >= 3 && len(deliveryPincode) >= 3 {
|
|
return SamePostalArea(pickupPincode, deliveryPincode)
|
|
}
|
|
if pLat != 0 && pLng != 0 && dLat != 0 && dLng != 0 {
|
|
return HaversineKM(pLat, pLng, dLat, dLng) <= MaxHyperlocalKM
|
|
}
|
|
return false
|
|
}
|