diff --git a/mapd_repo/cereal/custom/custom.capnp b/mapd_repo/cereal/custom/custom.capnp index 020219b2ae..0d2a464030 100644 --- a/mapd_repo/cereal/custom/custom.capnp +++ b/mapd_repo/cereal/custom/custom.capnp @@ -291,6 +291,11 @@ struct MapdOut @0xa4f1eb3323f5f582 { computedMonoTime @31 :UInt64; # Computation boot-time nanoseconds, not send time. sourceGeneration @32 :UInt64; producerSession @33 :UInt64; # Random nonzero process session. + # Data-only preview from the same qualified GPS fix; no controller smoothing. + previewSpeedLimit @34 :Float32; + previewDistance @35 :Float32; + previewBoundaryId @36 :Text; + previewWayId @37 :Int64; enum SampleStatus { unknown @0; diff --git a/mapd_repo/cereal/custom/custom.capnp.go b/mapd_repo/cereal/custom/custom.capnp.go index 32af399a4c..d6a256774c 100644 --- a/mapd_repo/cereal/custom/custom.capnp.go +++ b/mapd_repo/cereal/custom/custom.capnp.go @@ -2887,12 +2887,12 @@ type MapdOut capnp.Struct const MapdOut_TypeID = 0xa4f1eb3323f5f582 func NewMapdOut(s *capnp.Segment) (MapdOut, error) { - st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 112, PointerCount: 6}) + st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 128, PointerCount: 7}) return MapdOut(st), err } func NewRootMapdOut(s *capnp.Segment) (MapdOut, error) { - st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 112, PointerCount: 6}) + st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 128, PointerCount: 7}) return MapdOut(st), err } @@ -3260,12 +3260,54 @@ func (s MapdOut) SetProducerSession(v uint64) { capnp.Struct(s).SetUint64(104, v) } +func (s MapdOut) PreviewSpeedLimit() float32 { + return math.Float32frombits(capnp.Struct(s).Uint32(76)) +} + +func (s MapdOut) SetPreviewSpeedLimit(v float32) { + capnp.Struct(s).SetUint32(76, math.Float32bits(v)) +} + +func (s MapdOut) PreviewDistance() float32 { + return math.Float32frombits(capnp.Struct(s).Uint32(112)) +} + +func (s MapdOut) SetPreviewDistance(v float32) { + capnp.Struct(s).SetUint32(112, math.Float32bits(v)) +} + +func (s MapdOut) PreviewBoundaryId() (string, error) { + p, err := capnp.Struct(s).Ptr(6) + return p.Text(), err +} + +func (s MapdOut) HasPreviewBoundaryId() bool { + return capnp.Struct(s).HasPtr(6) +} + +func (s MapdOut) PreviewBoundaryIdBytes() ([]byte, error) { + p, err := capnp.Struct(s).Ptr(6) + return p.TextBytes(), err +} + +func (s MapdOut) SetPreviewBoundaryId(v string) error { + return capnp.Struct(s).SetText(6, v) +} + +func (s MapdOut) PreviewWayId() int64 { + return int64(capnp.Struct(s).Uint64(120)) +} + +func (s MapdOut) SetPreviewWayId(v int64) { + capnp.Struct(s).SetUint64(120, uint64(v)) +} + // MapdOut_List is a list of MapdOut. type MapdOut_List = capnp.StructList[MapdOut] // NewMapdOut creates a new list of MapdOut. func NewMapdOut_List(s *capnp.Segment, sz int32) (MapdOut_List, error) { - l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 112, PointerCount: 6}, sz) + l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 128, PointerCount: 7}, sz) return capnp.StructList[MapdOut](l), err } @@ -3395,312 +3437,318 @@ func NewMapdOut_GpsSource_List(s *capnp.Segment, sz int32) (MapdOut_GpsSource_Li return capnp.NewEnumList[MapdOut_GpsSource](s, sz) } -const schema_b526ba661d550a59 = "x\xda\x94Z}lT\xd7\x95?\xe7\xbd\xb1\xc7\x9f\x8c" + - "\x877\x04\x0c&\x13(\xd0\x98\xc6i\x8c\xf16\xa6I" + - "]c\x03\xc1\x1a\x83\xc7\xc3G@A\xea\xf5\xbc\xeb\x99" + - "\x07\xcf\xefM\xde{c{\x10\x11\x84\x82\x04YP\x93" + - "n\xd2\x92*lC\x02R\xda\x0d-\xed\x86\x15\x1bQ" + - "-AD\"Y\x90\x92\xaa\xa9\x9a4\xbbI\xa3JI" + - "\xba\x89\xd2v\x13m\xb6\xdb\xe8\xad\xce\xbd\xf3\xf1\xfc\xd1" + - "\x10\xff\xc3\xf8\xfd\xcey\xe7\x9e{\xee\xb9\xe7\x9e\xdf}" + - "\xdc\xf1\xf5\xfao\x86\xda\x1b3\x0d\xa0$\x1f\xae\xaa\xf6" + - "\xa3{v\xbccy?9\x00\xd1E\xe8\xef\xa8\xdb\xba" + - "x\xe4\xf9\x15\xe7!\x14\x06\xe8h\xae\xde\x89Z[u" + - "\x18T\xff\xa7\x1f'\xbav\xfe\xf1\xf2\x83\x93\xb5\xaa\x84" + - "Zm\xf5^\xd4\x16W\x87\x01\xb4\xe6\xeaw\x01\xfd{" + - "{/\x7fgx\xc5\x0f\x0fCt\x91RQ\x06\xec\xc0" + - "p?j\xf3\xc2\xf4R4l\x87\x00\xfd\xb6\x97\xf2\xf7" + - "\xfe\xb8\xf9\xf8#\x90l\xc3Z\xff\xb2\xf5o\xf5\xdb\xae" + - "\xdc\xf7\xdfP\x85\xa4s\xbc\xfe!\xd4\xce\xd4\xd3\x9f\xa7" + - "\xea\xdfR\x00\xfd\x7f\xfc\xcaGyc\xef\xce\xefA\xf4" + - "K\x8a\x7f\xf0\x93O\xbe\xd4\xf1_\x7f:M\x96_\x9f" + - "s\x10\xb5\x0f\xe7\x90\x0f\xef\xcf\xf9>\xa0?\xf79\xcc" + - "d\xae^\x7fb\x86iuE\x86Q\x1b\x88\xd0\xb4\xda" + - "~\x1bW\xfb\xc2\xd9S3h\xb5Fv\xa2v\xb7\xd0" + - "2\xd5\x8eo~:\\\xfd4$\x17\xe1\xb4\xd97\x93" + - "^[D\xbe\xb2\x1d\x01+~%\x17\xe1\x9c\x80:E" + - "\xa8\xe3d\xd3B\xd4\xce6}\x19\xa0\xe3\xa5\xa6\xf10" + - "\xa0\xdf\xd7{\xe9\xe5\xb3o=}fZ\xb4\x92\xcd\x07" + - "Q\xe3\xcd4'\xd6\xfc5@\x7f\xeb\xe9\xe4\xeb\xb7\x8d" + - "]?3\x83\xb7\xbcy'j\x85f\xf2\xf6'\xbfM" + - "?\xf6\xf7\xcf|\xf4O\xd3\xecmm^S\xb1\xb7\x19" + - "\xd0\xdfu\xfe\xdd\xf6\xef|\xf6\xd6Of\xb0\x97'{" + - "G\x85\xbd\xee\xb7\xfe\xf0\xc0\xc0-\xdb\xcf\xcd8\xeap" + - "i\xd4\xab\x98Xb\xa4\xad\x0b3hm%[\x86\xd0" + - "\xca^\xfc\x87GS_\xef\xfb\xc5\x0cZ\xeb\xc8\xd6\x0e" + - "\xa1uH]\xfd6\xbfk\xd9\xe5\x19\xb4:Ik\xa3" + - "\xd0\xfa\xe8\x9f\xbf\xf1\xea\x9dw\xef\xbe:uU\x14\xd2" + - "[\xde<\x17\xb5N\x9akG{s\x9cVe\xcd\x8e" + - "\xed9\xf3\xb5\x1f\xfe\xfbLC/\xa4\xa1\x17\x92\xd1W" + - "\x0e>\x99\xf9\xbf7\xbew}\xa6\xa1Ik\xa3\xd0Z" + - "|%\xfd\xe6\x9b\xde\xc1W\xa7\x85x\xf9\xc2\xb5\xa8u" + - ".\x14\xc3.\x14\xc9\xd0\xd5u\xf1\xd4\xb5\x87\xfe\xe7\xd7" + - "\xe4\xa5:\xc5\xe2\x92E\xfd\xa8u.\xa2\x05i_D" + - "\x1b\xa73\xd1\xf7\xed\x13\xdb\xbf\xf7\xc6\x0c\xa3\xcfk\xd9" + - "\x89Zk\x0b\x8d\xde\xf5\xed\x97\x7f\xf0\xfd\xaa\xef\xff'" + - "\x99T\xa6\xa8U\xb5\xacEm^\x0b\x99\x8c\xb6\x8c\x03" + - "\xfa\xb7\xbe\xfa\xeb\x8fn[\xf7\xdc\x9f\xa7\xed\x98|\xcb" + - "n\xd4\x8e\x0a\xcd\xc3-\x19\xc0\xca\xe6\x9blVn\xc5" + - "\x9f\x91\xf6\x8b-\xf3\x01:\xae\xb7\x84)m\x9bO\x9e" + - "\xdcX\xfb\xc1M\x1f\xcf\xe0\xebk\xcbv\xa2\xf6\xfe2" + - "\xf2\xf5\xe9P\xee\xb3\xaf\x1f:\xfe\xbf3h\xbdHZ" + - "\xaf\x0b\xad\x95\xef-\xdf\xf4\xee}\xdf\xf8\xcb\xb4x\x9e" + - "_6\x8c\xdaK\xcb\xc8\xc9\x17\x97\xed\x07\xf4\x9fr\x07" + - "\xaf\xbe\xbc\xeb\xe9\xbfLuR,z\xd5\xf2\x83\xa85" + - "/\x17\xe1Z.\xa2\x1f\xfd\x97\xf1#\x1f\xf6\x0c\xffu" + - "\x86\xe1\x1f[1\x8c\xda\x8fV\xd0\xf0\xfbO\x9c{7" + - "u\xe2\x88?5\x93\xc4\xcc\x0f\xafx\x1e\xb5\x93+\xc8" + - "\x85\xc7W\xfc\x14\xda\xfc4w83\xbf\x9a\x0e\xe5]" + - "\xcf\x1e\xfdjZ\xfc\xdc\x9ef9+\xb7\xa6W<\x0c" + - "q\x97;c\\\xd5W\x0f\"~\xae~\xcac\xce\xa0" + - "a\xda^/sz\xbbm\xcbsls\x101\xd9\xa4" + - "\x86\x00B\x08\x10e;\x01\x92\xdfR1i*\x18E" + - "\x8c!\x81\xc6w\x01\x92\xa6\x8a\xc9\x09\x05\xa3\x8a\x12C" + - "\x05 \x9a'MO\xc5\xe4\x01\x05\xa3\xaa\x1aC\x15 " + - "\xfa@?@r\x9f\x8a\xc9#\x0a\xfa\xd9\xbc\xdeKc" + - "\x80j\x9b8\x17\x14\x9c\x0b\xe8\xbb\x1e\xe7\x8eae0" + - "a\x8c\x1a\xdeFk\x04\xed\xb2\xccbcF\x86y\x06" + - "\xa8\xb6U\x06sY\xdb\xe2\x1br.\x00`S\xa5\x80" + - "\x03b\x13|\xfel\x07XN\xdfh\xe5\xf2\xde\x96B" + - "\x8e\x03\xd0D/\x92\xebZ\x9b\xd2\x0f\x80s\xb4V\xe5" + - "\xe7\x00\x18\xd1Z\x95\xa7\xc8\xb8\xd6\xaa\xfc\x18\x00\xa3Z" + - "\xabr\x19\x00\xe7j\xad\xca\x1b\x00X\xaf\xb5)\xc3\x00" + - "\xa8i\xad\xca\xcb\x00\x18\xd3\xda\xc4/j\xed\xca^\x00" + - "T\xb46e7\x00\xce\xd3Z\xc5\xf3M\xdar\xe5\x0f" + - "\x008_kU~\x09\x80\x0b\xb46\xe5\x1d\x00l\xd6" + - "\xda\xc5\xf3B\xadS\xf9\x01\x00.\xd2:\xc5\xb8-Z" + - "\xa7\xb0\xb7X\xeb\x12\xcf7k]\xc2/\xb5\xf8\x1c\xd2" + - "\xba\x84?q\xadK\xd8\xbdE\xbb[\xd8[\xd2\xd1\xa3" + - "\xacA\x00\xac\xd26*\xcf\x03`\xb5\xb6Q8\xd0\xa8" + - "\xadSv\x02`\x83\xd6#\x1c[\xaa\xdd-^\xfc\x92" + - "\xd6#\x0c/+\xfe.\xef\xe8Q\xe6\x92\x81:m\xa3" + - "\xf2\x10\x00\xae\xd06*\x7f\x06\xc0/w\x0c(KI" + - "p\xab\xb6U\x84\xa0\xb5c\x87\xa2\x10\xb0\xb2c\x97\xfc" + - "\xe3+\x1aS\xbe\x0b\x80\xb7iL\xbc\xda\xa61\x11\xbc" + - "p\xf1\xb9Fc\xcaA\x00\xac\xd5v\x89\xdf\xdb\xb5\x1d" + - "\xe4\x83\xaf\xdb\xe3\x96i3\x1d\x00|\x97{[\x98\x93" + - "\xe1\xe8%\x98\xc7\x1df\xc6{\xd2in\x12\x9e\xcaq" + - "\xae\xcb\x14\xd9<2\x12v\xb97\x05\xed\xb5\xad\x08e" + - "/\xc1\x03,\xd7\x9b\xafr\xc6\xb8\x90\xf7\xca\xb4\x06\x97" + - "{\xdb\x0c\xd7\xb0\xad\xde\xfc$\x91*_J\xd8\x99\x04" + - "\x87\xf0\x98\x1cOh*RU\xf8D.\xf5H\x1f\x83" + - "\xb2\x01\xc3\x92\xe2m\x00\xbe\xc3i&)\x0e\xdd\x9eg" + - "X\x19\xd7w\xd9\x18Oq\xcf\x83\x88|\xe4\xde:\x8b" + - "\x0d\x9b\xd0-\x87\x9fjl\xab\xcb\x85\x9c\xa7\"%\xb1" + - "\x98\x8a2I\x96\xe3\x1c\xf5\xf2\xec\x151\xfb\x804\\" + - "|\xf3\x1e\xdb\xd4\x13\x0as\xbd\x14\xe7\x96P%M\xf4" + - "\x02Q\x16h?W\x9d=S\xc1\x9et\xb8\x18x\x81" + - "*\x12\xddb\x8c\xf2\xcd##.\xf7d \xfa\xf8\x08" + - "\xcb\xa3\xe9%\x98\xc5\xb7\x1ba\xdd\xcb\x96]\xc6R\xdc" + - "\xe2\"p>\x05\x86\xd41oz\x14\x11#L\x01!" + - "t\x88\xa7\xed\xaa\xd1Qn\xe9\\\x17\x12+\xe3\xd2Z" + - "\xa5L{\xbc\xcf\x1e\xb7\xd6\xdb\xce&>!\x1dHD" + - "h\xb2\x95\xb9o\xcdM\x92\x1a\xe1\xa2\x94\xe6\x9eRK" + - "s\xf6\xb6g\x0d\x93\xf7f\x99\x951\xacL\x8awK" + - "u1\xfa w\\4\\\x8f[\x1e\x09\xe4\xb2\xa5\x99" + - "\x95\xe6f\x9f\x0d\xdd27\x8b\xe9\xd1\xefR\x1d*>" + - "\xa4l\x88\xe4\x9d4\x17\x8b:\xe1qG\xb1\x98Y\x0e" + - "\xf3\xa4t\x14b,\x89\xe3\x89Is\x90\xd9;\xe8\x18" + - "q\xdb1\xbcB\x19W\xa5\x19r\x9a\x0f\xf1\xfb\xf3\x86" + - "\xc3]\xda\x0d9\xf4|F\xbf^*\x87\xa5\xd1\xe4r" + - "\x0c:\xdcu\x95\x0d\xcc\xddb\xf7\x145&\x8d\xd7\xa3" + - "\xef\xce\xbb*\x85_\xaefP\xab\x12;\x01*^e" + - "*\xb4\xea\xb6\x9a\xf7\xcaC\xd4\x8a!6\x8fq\xc71" + - "t^Q\xa4U\xeb\xb5-\xdd\xf0\x0c{j0J;" + - "0\x95e\xba=\xde\xcb\x9c\x94\xc7<\x14\xd1\x13\x10\x8e" + - "\x0f\xd8:7\xb7\xad\x92S)b\x1brn\xc2N\xb3" + - "\x08\x19$\xb8\xdf\xb5\xadAd^v\xbdi\xb3\xe2\xac" + - "\x05V\xcd\xbc\xec\x16>!\\\x10\x00\xf3\xb2k\xed\xe0" + - "\x90ESd\x89\xd6#\xe2X\xcc,\x1f\x16U78" + - "\x1a\xc9Y~\xbb8s\xe8\xd0X]:\x1d\xb5]\xb8" + - "\x17 u\x1f\xaa\x98\xcab\xe5\x80\xd48\xf6\x03\xa4t" + - "\xc2sX9#\xb5Q\x1c\x02H\x99\x84O`\xe5\x98" + - "\xd4\xf2B\xdf#\xfc\x00\xe1\xa1\x9a\x18\x86\x00\xb4\x07p" + - "\x15@j\x82\xf0C\x84W\xd5\xc6\xb0\x0a@{\x10w" + - "\x02\xa4\x0e\x10~\x8c\xf0\xea\xba\x18V\x03hG\xf1\x07" + - "\x00\xa9c\x84\x9f <\\\x1f\xa3&B{\x0c\x1f\x02" + - "H\x9d \xfc4\xe15\x0d1\xac\x01\xd0N\x09\xfd\xd3" + - "\x84\x9f#\xbc\xb61\x86\xb5\x00\xdaYt\x00R\xcf\x12" + - "~\x81\xf0\xbap\x0c\xeb\x00\xb4\xf3\xf8\x14@\xea\x02\xe1" + - "WP\xc1%\xf5J\x0c\xeb\x01\xb4\x17p\x0d@\xea\"" + - "\xe1WQAl\x88a\x03\xf5L\xb8\x12 u\x89\xe0" + - "7QA\xdf\xb5i\xcb\x0c\xd8\xd0m\xd9\x94VX\x0b" + - "\x0a\xd6\x02\xfa&\xf3\x0c/\xafs:\xd3\xebA\xc1z" + - "\xc2l+C /c\xcc\x9c\xa6\x17w\xc56\xaa" + - "\x03\x05\xeb\x00\xfda\xce\xa8\x9b\xe8\x03\x95g\xca`\xd6" + - "v\x8c\xbd\xb6\xe5!3{\xd2\xe9\xbc\xc3\xd4t\xa1," + - "\x1c\xe3\x8eg\xa4\x99\x89R\x94.\xd0\x018\xd9Z\x0f" + - "\x16E}A\xab\xae\xac\x94i\x88\x0bY\x19\xcf[\xc6" + - "\x84\x98\x9d\xeb\xb1\xd1\xdc\x80a\x86M\xc3\xc5*P\xb0" + - "\x0a\xb0;\xcb\xdc\xf5\xc6\x04\"(\x88\x80\x91\xb1M\xeb" + - "\xfap\x0e\xe0\xa0\x8a\xc2\xc0\x9c@\x0fS\xf57z\x98" + - "\xcdy\xef\xf6\x14\x1b\xcd\x99\x9c\x123\xef\x8a\xa4\\ " + - "\x9a\xb0\x9e\xb5\xd4\x88D\xbbVQ\x1f\x12m\xa7S_" + - "\x8d\xb6\x11\x18\x8a.\xa7^\xa0*\xbad7\xc0\xfe\xbc" + - "\xb5\xc7\xb2\xc7\xad\xb8eo\xc8\xb9\xbee\xf7\xdac\xdc" + - "a\xa0f\xf8~\xcb\x1e`^:\xeb\x8f\xd2\xbf\\\xdf" + - "\x04\xdd\xb6,!E \x01\xb2\x0a\x7f\xe1>4\xac\xb7" + - "\xaf\xbaQ#:\xa5q\xed\xba\x91>\x85\x81\xf61\x9d" + - "\x1a\x9b\xd5\xbc'\"PnZ\x1f\x7f\x08 \xf9\x84\x8a" + - "\xc9g\x02M\xeb\x19jEO\xab\x98<\x17hZ\xcf" + - "\xae\x04H>\xa3b\xf2J\xa0i}\x814/\xa9\x98" + - "\xbc\xa6 \x86\xc4N\x8c\xbet\x10 yU\xc5\xe4\xaf" + - "h\x1b\xa2\xd8\x86\xd1W\x87\x01\x92\xaf\xa8\x98|S\xc1" + - "r'\x83\x83\x8e\x9d\xa1:)\xfa\xd32a(\xf6\xa7" + - "n\xe9\x88\xa3~\x0c\x14l\x00\x8c\xe4\x98\x97-\xe5@" + - "S\x85\xb1\x01\x8al\xc8\xd9\xae\xa8\xa8\xb2\xdf-\x93\xaf" + - "\xa2=\xd3\xb6sC\xcc\xe3\xd8C\x0b\x98\xe1\x95\xdc-" + - "I <`Xe\xb4\x14T\xf5o\xe5\x96\x9a\xf7\x92" + - "5\x18\xbc\xea\xa8\xdd\x1d`q\xb5C~)\xf5 B" + - "\xc9\xe7o\xc8\xb9)\xda\xd7\x80\x0f\x90" + - "\xbaF\xf8o\x08\xafVea|M\x0c\xfb\x0a\xe1o" + - "\x12\x1e\x0e\xc9\xc2\xf8\xba\xb0\xff\x1b\xc2\x7fOx\x8d*" + - "\x0b\xe3\xefT*\x8c\xbf'\xfc\x8f\x84\xd7\x86da\xfc" + - "P\xa5\xc2\xf8\x01\xe1\x9f\x12^W%\x0b\xe3'\xeaw" + - "\x01R\x9f\x12\x1e\x0a)\x18\xad\xaf\x96\x85\x11C\xbf\x04" + - "H\xd5\x84TL\xc5\x08ox[V\xc6h\x88\xfci" + - " |\x01\xe1\x8d\x8bc\xd8\x08\xa0\xcd\x0bQ\xc1o\"" + - "\xbc\x85\xf09\xbf\x8b\xe1\x1c\x00\xad9D~. |" + - "\x19\xe1\x91\x9a\x18F\x00\xb4%\xa1\x97\x01R\xb7\x12\xbe" + - "\x9a\xf0\xa6\xda\x186\x11\x9f\x0fQ|\xee \xfc.\xc2" + - "\xa3u1\x8c\x02h]!\x9a\xd7]\x84\xdfC\xf8\xdc" + - "H\x0c\xe7\x02h\xebB\xc3\x00\xa9>\xc2\x07\x09\xd7\xea" + - "c\xa8\x01h\x03\xa1\x9f\x03\xa4\x06\x09\xbf\x8f\xf0XC" + - "\x0cc\x00\xda\x8e\x10\x1d(\xf7\x11\x9e%|^c\x0c" + - "\xe7\xd1\xc1\x17\xa2\xf8\xe8\x84\xe7\x08\xbf\xa9%\x867\xd1" + - "\xc1'\xf4s\x84\xef#|\xfe;1\x9c\x0f\xa0\x15\x84" + - "?\xfb\x08?B\xf8\x82\xc51\\\x00\xa0\x1d\x0e\xed\x06" + - "H\x1d\"\xfca\xc2\x9bkb\xd8\x0c\xa0\x1d\x17\xf19" + - "B\xf8\xa3\x84/\xac\x8a\xe1B\x00\xed\x11\xe1\xe7\xa3\x84" + - "?I\xf8\xa2\x9bc\xb8\x08@;)\xfcy\x82\xf0g" + - "\x08o\x89\xc7\xb0\x05@;#\xe2y\x9a\xf0s\x84/" + - "^\x16\xc3\xc5\x94\x9e!:\xa0\x9f%\xfc\x02\xe17\xd7" + - "\xc5\xf0f:\xf8D|\x9e#\xfc\x12\xe1\xf1\xfa\x18\xc6" + - "\x01\xb4_\x88y]\"\xfc\x1a\xe1\xb74\xc4\xf0\x16\xca" + - "C\x81_#\xfc7\x84/i\x8c\xe1\x12\xca\xc3\xd0A" + - "\x80\xd4\xaf\x08\x7f;\xa4\xe0\xfeqV\xd8\xc4Fy\xa9" + - "\x14t\x8f\xb3\xc2\x10\x1f)=\xfa\x8e\xcdt\x92\x07\xaa" + - "\x85f \x09\xa5\x0b\xd3dJI" + - "V\xb4\x9b\xc6\x8ac\xb6\xc5\xb7\xb3B\xe9\xa0\x8c\x9b\xcc" + - "\xe2.V\x83\x82\xd5\x80\xbeg\x98" + - "\xe1a\xa4r\xc7\x09\x88\x11@_7\x8aQ]\xef\xd8" + - "\xa3\xdbY\xa17\xce-j\xfa\xcb=F\x91xb\x89" + - "y\x06<\x1a%\xbe\xe4\x8c\xf1\xa9\xf1\x1bg\x85\x147" + - "y\x1a\xa9\xf8\xcb\x1b\x0b\x8cT\xae\xaa\x8a#\x97\xe6\x8c" + - "\x86\xec\xdc\xbd`@\xb2F&;\xce\x0a\xbd\x101\x99" + - "\xebb\xa4rq(\xdf\x8e\x8f\xb3\xc2F\xbd\xd4\x9a\xf8" + - "\xe9b\xf3>\x85\xabT2O\x14\xfem\x1c\xe2\x0eM" + - "\x07\xc3\xa0`\xb8\x18\x1d\xeaD@\xcd\xd3 \xe5\x93\xa3" + - "\xe8b\xa6r@`\xa4r\x94T\xa4\x03\xb6eo\x81" + - "p\xb0!L\xdb\xa3\xb9\xbc\xc7u\x142\x83v@Y" + - "&\xbb\xc8\x0d\x1c-\xee0y2\x96e9\xc7\xd6\xf3" + - "i\xee`\x8a\xbb\xe4cE\xf2\xb9\xbd~\x99\xaaHz" + - "+\xa2M-E\x8dh\x12\xa2kDSU\xbb\x16\x80" + - "v\x8fg\xa4\xf7\xe7\xb8\x93\xe6\x967\x9b\x9e\xa63\xd8" + - "\xd3\xcct\xfcR\xde\x89K8>!\xfa\x99\x061\xf8" + - "b\xd9\xd1\xcd[):\xba\xc6\xb5\x00\xfbG\x1c\xce\xc7" + - "Y!\x926\xbcB\xa9\x93\x9b\x8d'\xed\xb3\xea\xc6\xc2" + - "z\xfb\xeaY\xb6o_\x9b\xed\x00\x9d\xb3}\xa1\xfdF" + - "\xc1\x14w}hQ\x1cc\xe5\xbe\xf0\x01\xea\xf6&T" + - "L\x1e\x0a\xf4\x85\x0f\xae\xaa\\Qb\xb1-<\xbc\x14" + - " y@\xc5\xe41j\x0b\x9bd[x\x94\xde>\xa4" + - "b\xf2\xe1J#\x12=N\xbd\xe21\x15\x93'\x14\x8c" + - "x\x85\x1c\xa5x\xf9\xfbSq\x97\x8d\x10[-\xed\xeb" + - "\xb0\xeb9\xe5\xceo\xd8\xb6\xcd\xf2n\xdd]$\xae\xc1" + - "b?\x9b\x98\xdc1\xdb v\xdc\xe8\x85{\x8a\xf5\x83" + - "\xaa\x87\xd8\x0fw\x8a\xe8\x9c\x92)\xf9x\xbfH\xc9\xc7" + - "v\x0b\x92\xf1\xc8*A2\x8e\x0e\x09\x92q\x98T\xaa" + - "\xa3\x0f\x0c\x03`8Z \xb0&\x9awh\xa7F\xef" + - "\xa7\xf7\xea\xa2\xa3\xf4^}\xd4\xa0\x9f\x86('\xcd\xc6" + - "(\x0b\x90\x13\x7f\xd4\xf6lg\x9c\x15\x00\xa0\xf2w$" + - "aX{\xe2\x9e\x93\xb7\xf6\xf8\xe2\xdf\x84a\x01\xee\xd9" + - "\x9fs\x8cQ\xe6\x14\xfc\xe2o\x02\xc2\x86\xb5\xc7w9" + - "\x155\xe6\x00\x16*\x7f\xc7\x0bd\xc3\xf7\x88\xf81G" + - "\x98/\xff-\xcc\xfby+Ms6 2bp\xdd" + - "w\xb8k\xe8\x9c\xca\xbf\xc1L\xdf4\xc6\x0c+C\xfd" + - "\xaf\xc3\xb9wC\x9aBK:h\x1b\x96'/\x0f\x02" + - "W\xeb\xfd3]\xad\x0f\x01$\xb3*&=j\x8eC" + - "2\x1d\xef'0\xa7br\x1f\xa5c\x95L\xc7\xc2\xde" + - "J2\x7fa\x02\x9d\xce;c\xcc\xcb;P9h}" + - "O^`r\xe86m*)\xd3\xb8\xc2\x17\xd8\xf1\x1d" + - "_\x84\xb0\x0d\x96\x98\x8c(\xad\xe58\xb4R\x1cnU" + - "1\xb9:\x10\x87v\x9a\xf2\x1d*&\xef\x9a\xcd\xecn" + - "\xe0\x00\x11gAW\xc4\xd5\x9dX\x0d\x11\xdf\xd6\x95\"" + - "\xa1\x97\xc8\x84^\xdc/\x12\xbay)@\xc4\xb2-\xee" + - "\x1bt\x96[\xcc\xa4D\xe1\x13\xe5\xbf\xc3i\xe6|\xe1" + - "\xaf+\xa9\x88\xc7<\x9e\x0ca\xe0\x9bs\x14\x97\x867" + - "\xe4\xdc\xe4\x812k\xba_\xa5\xb67G4b_\x90" + - "5\x15\x04\xad\xd9G\xf8\x91 k:\xacR[z\x88" + - "\xf0\x87\x09WQ\xd2\xa6\xe3\xeaS\x00\xa9\x87\x09\x7fB" + - "\xd0&U\xd2\xa6\xc7\x85\xfd\x13\x84\x9f\x16\xb4)$i" + - "\xd3)\x95\xda\xd2'\x09\x7fV\xd0\xa6*I\x9b~$" + - "\xec\x13\xc3\xfe" + - "\x95\xd4k\x04k\x8aH\xd6T%\xba\xfc\x10u\xe7M" + - "\x8255I\xd6\xd4(XD\x85\xad\xcd\x8dJ\xd64" + - "/\xf4\xfc$V\xa6\xa9\x925-\x09\x91\x9b\xcb\x08\xbf" + - "C\xb0\xa6\xb9\x925\xb5\x09;\xb7\x11~\xa7`M\x9a" + - "dM\x9dB\xffN\xc2\xfb\x04k\x8aI\xd6\xd4#X" + - "M\x85\x95\xcd\x9f'Y\xd3\x80\xb0\x93 \xfc^\xc1\x9a" + - "n\x92\xaci\xab`\x17[\x08\xff\x96`M\xf3%k" + - "\xda%\xd8`\x85\xad-\xfc\xa6dM\\\x8c\x9b%\xdc" + - "\x13\xac\xa9G\xb2\xa6\xfb\x85\x9d\x0a[kY+YS" + - "A\xe0\x13\x84\x1f\x12\xac\xa9W\xb2\xa6\x07\x05~\x80\xf0" + - "c\x825\xf5I\xd6tT\xf8Yfk\x18\x97\xa4\xe9" + - "\x91\xd0R\x80\xd41\x82O\x84\x14\x14\xf7\xd9\xe6\xa0\xc3" + - "!\xe2\xba\x81\xee\x95\x99\xe3\xac\xe0n\xb6\xb0\xf85h" + - "]\xb7\xf8\xc4QQ\x18v\xd8\x1e\x9e02\x10\xcez" + - "n\x19\xd5\x99\x9b\x1d\xb6\x99\x83z\xe0f\xbb\\\xd3t" + - ">\xe3X\xe5\x06^\xdcps\x1d\xa6K\x12\xb6\x95!" + - "ix\xa6\xf7\x94m\xdc)\x944\x82\xef\xef\xe7i{" + - "\x03gN\xf9\x8d\x11\xdbI\xf3^\x9b\x81\xeaze\xd0" + - "p\x07\x99\xb3\x87hAe\xe0\x1c\xcb\xbb<\xc1<\x88" + - "\xd0\xe4\xa7\xc0\xb6\x85\xa2\xfa\x19\x16C3@\x80l\xc7" + - "\xdb\xc0\x19`e<\xcfa##Fz\x00m]~" + - "#\x0a\x12\x84\x0csS\x1eK\xef\x09\x8e;j\xeb\x9c" + - "f\x01\x93&*\x0b\xdb\xa0\x03q\xfe\xb9\x8b\xd4\xd3m" + - "\x9a\xf6x0B\x95\xe5\xf0\xec\\\xca\xc8\xa0Ufq" + - "\xf2C\xcbt\xa3\x12O\xd8X\x8a'N\x95m\xe3X" + - "\x8a\xf7\x94ws\\g\xe6\x00\x9b\x80\xf8\x10\xcfpk" + - "2.\xe2\xd6\x93\xf6\x8c1^\x99\xb0\xc7]\x93\xf5\xf6" + - "\xae\x83n+\xc32\x01SEA\x1f\x1a.\x17\"\x9c" + - "&\xdb\x84\xb6\xd7\xe3\x8c\x06\x13Fd\xf3=4\xe1^" + - "'o\xb8\x81\x81D\xee\xcd(\xc9\xe5M\x97\xf7X:" + - "\xc47\x98\x86\xceKx8\x93sg\xf9\x8d{\xca\xb9" + - "|\xe7,;\xf7\xbf\xbb\x91\xfe\xf6\"+\x15\xa44\\" + - "\xc8\x89STv)\x9d\xb2-l\x1b\x12\xa7h\xab<" + - "E\x97\xf7\x8b\xb6p\xc9J\x80\xfd\xe9\xbc\xe3\x10i\xca" + - "9\\7\xd2\x1e\x07\xd4\xfd\x9c\xed\xba\xc6\xb0\xc9Kg" + - "\xab\xa5\x8b]\x10\x19a\x86y\xc3\x8b\xf0\xbe\xe2\x95k" + - "\xf9\xc6u\xca5\xf0\x1a\x80\xe4\xa3*&\x9f\x0c4\x16" + - "'\x87\x02w\xc3\x0aJ\xd7\xcf\xec\x0c\xdc\x0d\xab\x8al" + - "\xb0\xce\x1e\x04H>\xabb\xf2B\xe5\x1a\xf8<\xbd\xfd" + - "\x9c\x8a\xc9W\x02\xd7\xc0\xd7I\xf1\x9a\x8a\xc9\xf7\x14\xec" + - "f\"\xbb\xa6\xa4\xabIs\xad\xe4\x8e\xed1s\xbda" + - "\x82\xca]\xac\x01\x05k p}\xcc\xf5\xf5\x86\xc9]" + - "(K\xcc\xe2w+@\xb7tK\xdcP\xfcRP\x12" + - "a\x1f\xf7\x98a\xbaP\xb9F.\xff\xa7\x92\xe25\xf2" + - "l\x88\xc1\xa4,\xb8Q\xf0K\x9f\xd5\xfa\xba\xa5\x0f\x82" + - "\xba\x96\xd7`\x1d5w}*&\x07\x15,-\xc1\x00" + - "E;\xa1b\xf2\xde\xc0\x12l\xa5 nQ1\xf9-" + - "%0\xe5\x00\x15\x9a]\xd4\xfe?\x00\x00\xff\xff\xd6\xbd" + - "\x94\xef" +const schema_b526ba661d550a59 = "x\xda\x94zkp\x1c\xd5\x95\xff9\xdd3\x1a==" + + "\x1a\xdf1\xd8\x92\xc5\xd8\xc6\xc2\xc8A\xc4F\xf8\x1f\xe4" + + "\xe0\x08Y2\xb6U#\xacV\xdb\x16v\xe1\xaa\\M" + + "_\xcd\xb4\xdd\xea\x1ew\xf7H\x1a\x17.\x1b\xc7T\x01" + + "\x7f\xa8@6\x0f'\x156\xfe\xfa\x03\xff\xfd" + + "\x13\xf2R]\x98bkG\x90\xedXK\x0b\xd2\xbf\x96" + + "6\xce\xb6\xec\xf0\xe7\xceM|\xf9\xe7K\x8c\xde\xd5u" + + "\x18\xd9\xd6.\x1a\xbd\xffs\xaf}\xed+\xf1\xaf\xfc;" + + "\x99T\x16\xa8\xb5u\xedD\xd6\xd5%\xf7b\xd7,`" + + "p\xfd\x9b?y\xff\x86]\xcf\xfdq\xd1\x8e9\xd9u" + + "\x14\xd9#R\xf3\xa1\xae<`}\xf3\xcd7\x1bn\xc5" + + "\x7f!\xedK]W\x03\xf4\xbd\xd5\x95H\x00\x06k\x1e" + + "}to\xd3{W}\xb0\x84\xaf\xbf\xec>\x8c\xec\x0f" + + "\xdd\xe4\xeb\x93\xb1\xe2\x87\x9f>\xfb\xd0\xff,\xa1u\x89" + + "\xb4~%\xb56\xff\xa6\xfb\x8ew\xef\xfa\xcc_\x16\xc5" + + "\xf3\xfb\xdd\x93\xc8\xde\xec&'/u\x9f\x02\x0c\x9e\xf0" + + "\xc6.\xbev\xe4\xc9\xbf,tR.z\xdbug\x90" + + "\xad\xbfN\x86\xeb:\x19\xfd\xd4?\xcf\xde\xf7\xfb\xc1\xc9" + + "\xbf.1\xfc\xa3\x9b&\x91\xfd\xe3&\x1a\xfe\xd4\xb9g" + + "\xdf\xd5\xcf\xdd\x17,\xcc\xa4\xb0\x08mz\x01\xd9S\x9b" + + "\xc8\x85\xc77}\x17z\x83\x9cp\x05\xb7>\x99\x8b\x95" + + "<\xdf\x99\xfedN\xfe\xdc\x98\xe3E\xbb\xb8}H>" + + "\x8c\x0bO\xb83B5n\x1eC\xfcH}\xdd\xe7\xee" + + "\x98i9\xfe\x10w\x87\x06\x1c\xdbw\x1dk\x0cQk" + + "Wc\x001\x04H\xf1\xc3\x00\xdagU\xd4,\x05S" + + "\x88i$\xd0\xfc\x02\x80f\xa9\xa8\xcd)\x98R\x944" + + "*\x00\xa9\x12i\xfa*j\xa7\x15L\xa9j\x1aU\x80" + + "\xd4\xc9\x11\x00\xedn\x15\xb5\xfb\x14\x0c\x0a%c\x88\xc6" + + "\x00\xd5\xb1p%(\xb8\x120\xf0|!\\\xd3\xcec" + + "\xd6\x9c6\xfd\xbd\xf6\x14:5\x99\xcdg\xcc<\xf7M" + + "P\x1d\xbb\x06\x16\x0b\x8e-v\x17=\x00\xc0\xf6z\x01" + + "\x07\xc4v\xf8\xe8\xd9\x8e\xf2\xa2\xb1\xd7.\x96\xfc\xfd\xe5" + + "\xa2\x00\xa0\x89\xbeH\xae\xb3^e\x04\x00W\xb0\x1e\xe5" + + "{\x00\x98d=\xca\x13d\x9c\xf5(\xdf\x06\xc0\x14\xeb" + + "Q^\x06\xc0\x95\xacG\xf99\x00\xb6\xb0^e\x12\x00" + + "\x19\xebQ^\x03\xc04\xeb\x95\xbf\xc8\xb6*'\x00P" + + "a\xbd\xcaQ\x00\\\xc5z\xe4\xf3U\xac[\xf9\x1d\x00" + + "^\xcdz\x94\x1f\x01\xe0j\xd6\xab\xfc\x1a\x00\xd7\xb0\xad" + + "\xf2\xb9\x83mS\xbe\x06\x80\x9dl\x9b\x1cw-\xdb&" + + "\xedu\xb1~\xf9|\x0d\xeb\x97~\xa9\x95\xe7\x18\xeb\x97" + + "\xfedX\xbf\xb4\xbb\x8e\xed\x90\xf6\xd6\xf7\x0d*\xdb\x11" + + "\x00\xe3l\xaf\xf2\x02\x006\xb0\xbd\xd2\x816\xb6K9" + + "\x0c\x80\xadlP:\xb6\x81\xed\x90/^\xcb\x06\xa5\xe1" + + "\x8d\x95\xdf\xee\xbeAe%\x19hf{\x95\x07\x00\xf0" + + ":\xb6W\xf9#\x00n\xea\x1bU6\x90\xe0zv@" + + "\x86\xa0\xa7\xef\x90\xa2\x10\xb0\xb9\xefH\xf8\xc7'\x18W" + + "\xbe\x00\x8070._\xede\\\x06/Qynd" + + "\\9\x03\x80M\xec\x88\xfc\xbd\x91\x1d\"\x1f\x02\xc3\x99" + + "\xb5-\x87\x1b\x00\x10x\xc2\xdf\xcf\xdd\xbc@?\xcb}" + + "\xe1r+3\x98\xcb\x09\x8bp\xbd(\x84\x11\xa6\xc8\xbe" + + "\xa9\xa9\x84'\xfc\x05\xe8\x90c'){\x09\x1e\xe5\xc5" + + "\xa1R\xdc\x9d\x11R>\x14\xa65x\xc2?hz\xa6" + + "c\x0f\x95\xe6\x89\xd4\xf0\xa5\xac\x93\xcf\x0aH\xcc\x84\xe3" + + "IM%T\x95>\x91K\x83\xa1\x8fQ\xd9\xa8i\x87" + + "\xe2\x83\x00\x81+h&\xba\x80\x01\xdf7\xed\xbc\x17x" + + "|F\xe8\xc2\xf7!\x19>\x0a\x7f\x97\xcd'-\x18\x08" + + "\x87_h\xec\x80'\xa4\\\xe8\xc9\xaaXNE\x99'" + + "+\x0a\x81Fm\xf6\x8a\x9c}D\x9a\xa8\xbc\xb9\xc7\xb1" + + "\x8c\xac\xc2=_\x17\xc2\x96\xaa\xa4\x89~$\xca\x12\x1d" + + "\x11\xaa{l!8\x98KT\x02/Q%D\xf7\x9b" + + "\xd3b\xdf\xd4\x94'\xfc0\x10\xc3b\x8a\x97\xd0\xf2\xb3" + + "\xdc\x16\x13f\xc2\xf0\x0b5\x97\xb1\x1a\xb7\x8c\x0c\\@" + + "\x81!u,Y>E\xc4LP@\x08\x1d\x179'" + + ">=-lC\x18Rb\xe7=Z+\xddrf\x87" + + "\x9dY\xfbv\xc7\xbdC\xcc\x85\x0ed\x934\xd9\xfa\xdc" + + "\x0f\x14\xe7I\xcdDEJs\xd7\xd5\xea\x9c\xfd\x89\x82" + + "i\x89\xa1\x02\xb7\xf3\xa6\x9d\xd7\xc5@\xa8.G\x1f\x13" + + "\xae\x87\xa6\xe7\x0b\xdb'A\xb8l9n\xe7\x845\xec" + + "\xc0@\x98\x9b\x95\xf4\x18\xf1\xa8\x0eU\x1et\x07\x92%" + + "7'\xe4\xa2\xce\xf9\xc2Uln\xd5\xc2\x87\xf5c\x92\x95\xa4\xbeO" + + "\xf8i\xc2c\x8di\x8c\x01\xb0\x93x\x13\x80>G\xf8" + + "Y\xc2\xe3Mi\x8c\x03\xb0{\xf00\x80~\x9a\xf0\x07" + + "\x09ohNc\x03\x00\xbb\x1f\xbf\x06\xa0?H\xf89" + + "\xc2\x13-ij\"\xd8\x97\xf0\x01\x00\xfd\x1c\xe1\xe7\x09" + + "olMc#u\x14R\xff<\xe1\xcf\x12\xde\xd4\x96" + + "\xc6&\x00\xf6\x0c\xba\x00\xfa\xd3\x84_ \xbc9\x91\xc6" + + "f\x00\xf6<>\x01\xa0_ \xfc\x15Tp}\x8b\x92" + + "\xc6\x16\x00\xf6\x03\xdc\x0e\xa0\xbfH\xf8ET\x10[\xd3" + + "\xd8\x0a\xc0~\x88\x9b\x01\xf4\x97\x08~\x1b\x15\x0c<\x87" + + "\xb6\xcc\xa8\x03\x03\xb6Ci\x85M\xa0`\x13``q" + + "\xdf\xf4K\x86\xa03\xbd\x05\x14l!\xcc\xb1\xf3\x04\x02" + + "\x8a\x1a\xc6\xadEz\x19On\xa3fP\xb0\x190\x98" + + "\x14\x9c\xba\x89aPE\xbe\x06\x16\x1c\xd7<\xe1\xd8>" + + "rk0\x97+\xb9\\\xcd\x95k\xc2\x19\xe1\xfaf\x8e" + + "[\x18\x8are:\x00\xe7[\x1b\xc4\x8ah8j\xd5" + + "\x0b+e\x0e2RV\xc3K\xb69'g\xe7\xf9|" + + "\xba8jZ\x09\xcb\xf40\x0e\x0a\xc6\x01\x07\x0a\xdc\xbb" + + "\xdd\x9cC\x04\x05\x1109s\xc7\xaea\\\x018\xa6" + + "\xa24\xb0\"\xd2\xc3\xc4\xffF\x0f\xb3\xaf\xe4\xdf\xa8\xf3" + + "\xe9\xa2%(1K\x9eL\xca\xd5\xb2\x09\x1b\xdcI\x8d" + + "H\xaa\xff&\xeaCR[\xe9\xd4WS\xbd\x04\xc6R" + + "\xdd\xd4\x0b\xc4S\xeb\x8f\x02\x9c*\xd9\xc7lg\xd6\xce" + + "\xd8\xce\xee\xa2\x17\xd8\xce\x903#\\\x0ej^\x9c\xb2" + + "\x9dQ\xee\xe7\x0a\xc14\xfd+\x8c;`\xc0\x09KH" + + "\x05\xc8BX\x85?v\x1f\x9a0\xb6\xdet\xa5Ft" + + "A\xe3\xda\x7f%}\x0a\x03\xedc:5\xf6\xa9%_" + + "F\xa0\xd6\xb4~\xf5\x01\x00\xed\xeb*j\xdf\x8c4\xad" + + "OQ+z^E\xed\xd9H\xd3\xfa\xccf\x00\xed\x9b" + + "*j\xafD\x9a\xd6\x1f\x90\xe6K*j\xaf+\x881" + + "\xb9\x13S\xaf\x9e\x01\xd0.\xaa\xa8\xfd\x98\xb6!\xcam" + + "\x98zs\x12@{CE\xedm\x05k\x9d\x0c\x8e\xb9" + + "N\x9e\xea\xa4\xecOk\x84\xa1\xd2\x9fz\xd5#\x8e\xfa" + + "1P\xb0\x150Y\xe4~\xa1\x9a\x03\xedu\xc6\x06(" + + "\xb3\xa1\xe8x\xb2\xa2\x86\xfdn\x8d|U\xecY\x8eS" + + "\x1c\xe7\xbe\xc0AZ\xc0\xbc\xa8\xe7nU\x02\x89Q\xd3" + + "\xae\xa1\xd5\xa0\xaa\x7f+\xb7\xd4\x92\xaf5b\xf4\xaa\xa3" + + "\xe9h\x84\xc55\x8d\x07\xd5\xd4\x83$%_\xb0\xbb\xe8" + + "\xe9\xb4\xaf\x01\x85v\xaeV\x19\xd7\xc4v\x02\xe8\xe9\x98" + + "\x8a\xfa\xbaX\xa42v\xc5\xa8R\xac&|c,R" + + "\x19\xd7\xc7\xa8\x02\xae#\xfc\x86\x98\x82X)\x8c=1" + + "*t\xd7\x13|3\xa9\xc70,\x8c[cTx\xb7" + + "\x10~+\xe1q%,\x8c\xfd\xb1\x17\x00\xf4[\x09\xdf" + + "Cx\x83\x1a\x16\xc6]r\xd8\xdb\x08\xcf\x12\x9e\x88\x85" + + "\x85q\xaf\xb4\xbf\x87\xf0\xfd\x847\xaaaa\xd4bT" + + "\x18\xf7\x13\xfeY\xc2\x9bbaa<\x12\xa3\xc2x\x17" + + "\xe1\x05\xc2\x9b\xe3aa\x14\xb1/\x00\xe8\x05\xc2}\xc2" + + "[\x1a\xc2\xc2x<\xf6#*\xe0\x84\x9f%\xbc\xf5r" + + "X\x19\xef\x91\xfe\xdcM\xf8}\x84\xb7u\xa5\xb1\x0d\x80" + + "\xdd\x1b\xa3\x82\x7f\x9a\xf0\x07\x09_\xf1\xab4\xae\xa0\xc2" + + ".\xfd\xbc\x8f\xf0/\x12\x9elLc\x12\x80=\x12{" + + "\x8d\x0a;\xe1\xe7\x09ooJc;\x15v\x19\x9f\xc7" + + "\x08\x7f\x9a\xf0Ts\x1aS\x00\xec[r^O\x13~" + + "\x81\xf0\x95\xc94\xae\xa4\xc2\x1e\x9b\x04\xd0\x9f#\xfc%" + + "\xc2YK\x1a\x19\x00\xfb~\xec{T\xc1\x09\x7f\x9d\xf0" + + "tk\x1a\xd3\x00\xec\xd5\x18\x1d(\xaf\x13\xfeS\xc2W" + + "\xb5\xa5q\x15\x00{K\xc6\xe7\xc7\x84_&\xfc\xaa\xb5" + + "i\xbc\x0a\x80\xfdR\xea_&\xfc=\xc2\xaf\xfeu\x1a" + + "\xaf\x06`\xbf\x95\xfe\xbcG\xf8\x9f\x09_\xdd\x95\xc6\xd5" + + "\x00\xecO\xb1\xa3\x00\xfa\x071\x15\xc7\xe3\x0a\xa6\xd64" + + "\xa6q\x0d\x00\xfbP\x86\xe7\xcf\xa4\x1e#\xbc#\x9e\xc6" + + "\x0e\x00\x86qr3\x16WQo'\xbc\xf3\x9a4v" + + "\x02\xb0\xb68\xb9\xd3J\xf8j\xc2\xd7f\xd2\xb8\x16\x80" + + "\xad\x8aS8\xd3\x84\xaf#\xbckc\x1a\xbb(;\xe3" + + "t>\xaf%\xfcz\xc2\xafiN\xe35\x00\xac;N" + + "\xe1\xd9H\xf8\x16\xc23-i\xcc\x10q\x8c\xd3\xb4\xb6" + + "\x10~+\xe1\xebZ\xd3\xb8\x8e\xd2P\xe2\xb7\x12\xbe\x87" + + "\xf0\xf5mi\\Oi\x18?\x03\xa0\x0f\x13>F\xf8" + + "\x06\x96\xc6\x0d\x00l4N\xe93F\xf8]\x84_\xbb" + + "6\x8d\xd7\x02\xb0CR\xffN\xc2\x0d\xc276\xa4q" + + "#\x00\xe3R\xdf \xbcHxw2\x8d\xdd\xd4_\xc4" + + ")l\x16\xe1sq\x05O\xcd\xf2\xf2\x1d|ZT+" + + "\xcd\xc0,/\x8f\x8b\xa9\xeac\xe0:\xdc y\xa4\x18" + + "\x85\xa7Z\xd6\x9c\x06\xd5\xf4kU\xc3\xae\xf4\xd80\x10" + + "vt\x8b\x04\x18\xe2\xc3\xe6\x80\xe7S\xf7\\U\x18(" + + "\xf0\x13\xdc5j\xd6I\x7f\x0f?\xc1A]\x02D\xd7" + + "\x186\xe9}\xb5n \xe0\xc6\x8c\xe99n\x192\xfa" + + "\xbc\x83\x9e^\x1a4fL$a\xe8\xc2\"\x99R\x95" + + "U\xec\xe6\xb0\xee\x98c\x8b\x09^\xae\x9e\xc3\x19\x8b\xdb" + + "\xc2\xc3\x06P\xb0\x010\xf0MKd\x89\x95\xaa\xc2\xa8" + + "\xaa\xd4\"\xa3\x98\xbe^\xca\xe7\x85\xe7\x0bC\x1a\x8f4" + + "\x0c^E\x00\x03\xc6|w\x85\xe7\x9b\xd3T\xa7\x8dq" + + "\x87\x1b\x13\xa6\xa1\xfa\x85\x9a\x90\xd6\x81\x9adH\x889" + + "\x1f\x93\xf5+T@L\x02\x06\x86Y\x89\xea\xed\xae3" + + "=\xc1\xcbC\x19a\x13\xa7\xa8\xb50\x15^\x8bUb" + + "\x1b\xf1h\x9a\xe8\x98;#\x16\xc6o\x96\x97ua\x89" + + "\x1c\xd2\xd9\x12^\x88`\xb2~\x13V\x19\xb9:g4" + + "Cb\xe0G\x03R0\xf3\x85Y^\x1e\x82\xa4\xc5=" + + "\x0f\x93\xf5{\xc9\xf0\xed\xcc,/\xef5\xaa\x9dO\x90" + + "\xabp\x83\x05T\xa8\x9ey\xf2\\9( \xe3\xd2t" + + "0\x01\x0a&*\xd1\xa1F\x07\xd4\x12\x0dR;\x98*" + + ".\xe6\xeb\xe7\x0f&\xeb'U]:\xea\xd8\xce~H" + + "D\xfb\xcd\x9c3],\xf9\xc2@)3i\x07\xd4d" + + "a\x93\xba[\xa0-\\\x1e\x1e\xbc5Y\xd1u\x8cR" + + "N\xb8\xa8\x0b\x8f|\x8cJ\xc4\x8c)fu\xac3\xed" + + "Z\xa8+\xb2a\xac,#,\x94\xecD\xa7d\x1b\xdc" + + "-\xef\xc5\xfa\xae\xa8\xc8& 9?\x8a\x1f\xc5Yj" + + "\x94+\xa4\xe9rY\xa95j\x94\xcdNj\xbbl\x0e" + + "\x9bv\x02\xd06\xf5\xcd\xdc\xa9\xa2ps\xc2\xf6\x97\xd3" + + "\x9bm\x8b\xf6fK\xb5\x11\x94\xe0\xf22Q\xcc\xc9\xbe" + + "\xacU\x0e\xde\x15v\xa6\xab6\xcb\xce\xb4m'\xc0\xa9" + + ")W\x88Y^N\xe6L\xbf\\\xedH\x97\xe3\xc9\xd6" + + "eu\x95\x09c\xeb\xcd\xcblC?\xb5\xdc\x01\xb6-" + + "\xf7\x85\xadW\x0a\xa6\xbc\xb3D\x9b\xe2\x98\xae\xf5\xb7'" + + "\xa9k\x9dSQ;\x1b\xe9o\xef\xb9\xa9~\xd5\x8a\x95" + + "\xf6\xf6\xde\x0d\x00\xdai\x15\xb5\x07\xa9\xbdm\x0f\xdb\xdb" + + "\xfb\xe9\xed\xb3*j\x0f\xd7\x1b\xaa\xd4C\xd4\xf3>\xa8" + + "\xa2vN\xc1\xa4_.\xd2^\xaa}G\xabl\xe7)" + + "b\xdd\xd5\xdcMx\xbe[\xeb`'\x1d\xc7\xaa\x95\x85" + + "\xa3\x15\x02\x1e=U\x96\x13\x93-\xcb\x0db\xdf\x95^" + + "\xd8S)TT\xa6\xe4~\xb8EF\xe7\xf10%\xbf" + + ":\"S\xf2KG%Yz\xe4&I\x96\xee\x1f\x97" + + "d\xe9^RiH\x9d\x9c\x04\xc0D\xaaL`c\xaa" + + "\xe4RIH\x1d\xa7\xf7\x9aS\xd3\xf4^K\xca\xa4\x9f" + + "\xd6\x94 \xcd\xb6\x14\x8f\x90\xac`\xda\xf1\x1dw\x96\x97" + + "\x01\xa0\xfew2k\xda\xc72\xbe[\xb2\x8f\x05\xf2\xdf" + + "\xaci\x03\x1e;Ut\xcdi\xee\x96\x83\xcao\x16\x12" + + "\xa6},\xf0\x04UO\xee\x02\x96\xeb\x7fg\xcad#" + + "\xf0\x89\xc0rW\x9a\xaf\xfd-\xcd\x07%;Gs6" + + "!9e\x0a#p\x85g\x1a\x82\xce\x19\x93[\x81e" + + "\xce\x98v\x9e\xfaxW\x08\xff\x8at\x8b\x96t\xcc1" + + "m?\xbc\x04\x89|\"\x18Y\xea\x13\xc18\x80VP" + + "Q\xf3\xa9\xc9\x8f\x85\xe9x\x9c\xc0\xa2\x8a\xda\xdd\x94\x8e" + + "\xf10\x1d\xcb'\xea\xc9\xfc\xb1/\x02r%w\x86\xfb" + + "%\x17\xea'z\xe0\x87\x17\xb1\x02\x06,\x87J\xca\"" + + "\xce\xf31v|\xdf\xc7!\x9ecUF&Kk-" + + "\x0e=\x14\x87\xebU\xd4n\x8e\xc4a+My\x8b\x8a" + + "\xda\xad\xcb\x99\xdd\x15\x1c\xd8W\xf2o\x94\xb4K^A" + + "\xca\xd5\x90\xf1\xed\xd9,\x13z}\x98\xd0]#2\xa1" + + "\xd7l\x00H\xda\x8e-\x02\x93\x9a\x06\x9b[\x94(b" + + "\xae\xf6w\"\xc7\xdd\x8f\xfd\x95HO\xfa\xdc\x17Z\x0c" + + "#\xdf\xceS\xb8!\xb1\xbb\xe8i\xa7k\xec\xef\xb8J" + + "}hQUQ\xbf[\x8d\xb0\xbf\xb2J\xf4\xecn\xc2" + + "\xefS#\xec\xef^\x95\xfa\xeb\xb3\x84?L\xb8\x8a!" + + "\xfd{H}\x02@\x7f\x98\xf0\xaf\x13\x1eSC\xfa\xf7" + + "Ui\xff\x1c\xe1\xe7\x09\x8f\xc7B\xfa\xf7\xb8J\xfd\xf2" + + "c\x84?MxC<\xa4\x7f\xdf\x92v\x9e&\xfc\x02" + + "\xe1\x89\x86\x90\xfe=\xaf\xbe\x0c\xa0_ \xfc\x15\xc2\x1b" + + "\x13!\xfd\xfb\x81J\xec\xf5E\xc2/\x12\xde\xd4\x18\xd2" + + "\xbf\x1f\xaa\xc4\x1b^!\xfc\x0d\xc2\x9b\x9bB\xfawI" + + "%\xf6\xfa:\xe1?%\xbc\xa59\xa4\x7foI?\x7f" + + "L\xf8e\xc2[[B\xfa\xf7K\x95\xfa\xf7\xcb\x84\xbf" + + "Gx[kH\xff~\xab\x12\xff\xf8\x0d\xe1\x1f\x10\xbe" + + "\xa2-\xa4\x7f\x7fP\x89&}\xa0\xaa8.\xd9\xdf\x8a" + + "\x90\xfd}(\x87\xfd+\xa97J\xf6\x97\x0c\xd9_\\" + + "\xb2\xb9\x18\xd1\xa4v\xc9\xfe\xdaC\xf6\xd7&\xd9Y+" + + "\xe1\xab%\xfbK\x85\xeco\x95d\xcduR\xce\xd4\x90" + + "\xfd\xad\x97\xacv#\xe1[$\xfb[\x19\xb2\xbf^i" + + "\xe7\x06\xc2o\x91\xec\x8f\x85\xeco\x9b\xd4\xbf\x85\xf0a" + + "\xc9\xfe\xd2!\xfb\x1b\x94,r\x98\xf01\xc9\xfeV\x85" + + "\xecoT\xda\xc9\x12~\xa7d\x7fW\x85\xec\xef@\xec" + + "\xcc<\xf6\xbd\xe6\xea\x90\xfe\x1d\x91\xac\xb6\xce\xbe;n" + + "\x0b\xe9\xdfB\xf6\xdd9\x18\xd2\xbf\xe3\xd2N\x91\xf0\xbb" + + "\x09_\xbb3\xa4\x7fe\x89\xd7Yy\xd7PH\xff\xee" + + "\x91x\x9d}_3\x1c\xd2\xbf\xfb\xa5\x9f5\xf6\x8d\x99" + + "\x90\xfd=\x12\xdb\x00\xa0?H\xf0\xb9\x98\x82\xf2^\xde" + + "\x1as\x05$=/\xd2&sk\x96\x97\xbd}6V" + + "\xbej\xed\x1a\x90\x9fj\xea\x0a\x93.?&\xb2f\x1e" + + "\x12\x05\xdf\xab\xa1\x06\xf7\x0a\x93\x0ew\xd1\x88\xdc\xd0\xd7" + + "j\x9a!\x96\x1c\xab\xc6\x14\xe4M\xbd0`\xb1$\xeb" + + "\xd8y\x92&\x96zO9(\xdcrU#\xfa\xfe)" + + "\x91sv\x0b\xee\xd6\xde\x98r\xdc\x9c\x18r8\xa8\x9e" + + "_\x03Mo\x8c\xbb\xc7\x88\x7f\xd4\x07.\xf2\x92'\xb2" + + "\xdc\x87$M~\x01\xec\xd8(\xab\x9fis\xb4\"L" + + "\xcbq\xfd\xdd\x82\x03\xd6\xc7\xf3]>5e\xe6F\xd1" + + "1\xc2o]Q&\x92\xe7\x9e\xee\xf3\xdc\xb1\xe8\xb8\xd3" + + "\x8e!h\x160o\xa2aa\x1bs!#>r\x91" + + "\x06\x07,\xcb\x99\x8dF\xa8\xbe\x1c\xbeS\xd4\xcd<\xda" + + "5\xba\x18~0Zl4\xc4\xb3\x0eV\xe3\x89\x0be" + + "\x07\x05V\xe3\xbd\xe0\xdd\xa20\xb85\xca\xe7 3." + + "\xf2\xc2\x9e\x8f\xcb\xb8\x0d\xe6|sF\xd4'\xec\x0b\xcf" + + "\xe2CC\xbb`\xc0\xce\xf3|\xc4TE@\xfcCH" + + "\x11.\x92\xdd\x81\x8e?\xe8NG\x13Ff\xf3\x1e\x9a" + + "\xf0\x90[2\xbd\xc8@2\xf7\x96\x94\x14K\x96'\x06" + + "m\x032\xbb-\xd3\x10U<\x91/z\xcb\xfcV\xbf" + + "\xe0\\\xbee\x99\x9d\xfb\xff\xbb\x92\xfeD\x85\xfeJ\xf6" + + "\x9b(\x17\xe5)\x1av)\xdb\xc2\xb6\xb0w\\\x9e\xa2" + + "=\xe1)\xda=\"\xdb\xc2\xf5\x9b\x01N\xe5J\xaeK" + + "\xa4\xa9\xe8\x0a\xc3\xcc\xf9\x02\xd0\x08\x8a\x8e\xe7\x99\x93\x96" + + "\xa8\x9e\xad\xb6!wAr\x8a\x9b\xd6\x15/\xf4\x87+" + + "W\xc7\xb5\x9b\xe3\x05\xd7\xd9\xdb\x01\xb4/\xaa\xa8=\x16" + + "i,\x1e\x1d\x8f\xdcq+\x18\xba\xfe\xd4\xe1\xc8\x1d\xb7" + + "\xaa\x84\x0d\xd63g\x00\xb4\xa7U\xd4.\xd4\xaf\xb3\x9f" + + "\xa7\xb7\x9fSQ{#r\x9d}\x89\x14_WQ\xfb" + + "\x8d\x82\x03\\f\xd7\x82t\xb5h\xae\xf5\xdcq|n" + + "\xddnZ\xa0\x0a\x0f\x1bA\xc1F\x88\\\x83\x0b\xe3v" + + "\xd3\x12\x1e\xd4$V\xe5\xfb\x1b\xa0W\xbd\xedn\xad|" + + "\xf1\xa8\x8apX\xf8\xdc\xb4<\xa8_\x87\xd7\xfesL" + + "\xe5:|9\xc4`^\x16\\)\xf8\xd5\xcf\x83\xc3\x03" + + "\xa1\x0f\x92\xba\xd6\xd6`\x175w\xc3*jc\x0aV" + + "\x97`\x94\xa2\x9dUQ\xbb3\xb2\x04\x07(\x88\xfbU" + + "\xd4>\xabD\xa6\x1c\xa1B\xcb\x8b\xda\xff\x05\x00\x00\xff" + + "\xff1<\xb37" func RegisterSchema(reg *schemas.Registry) { reg.Register(&schemas.Schema{ diff --git a/mapd_repo/cereal/testdata/mapd_preview_wire.json b/mapd_repo/cereal/testdata/mapd_preview_wire.json new file mode 100644 index 0000000000..c5403af2e7 --- /dev/null +++ b/mapd_repo/cereal/testdata/mapd_preview_wire.json @@ -0,0 +1 @@ +{"description":"Synthetic Go-produced v2 current-road and data-only preview fixture; no real road data.","event_base64":"AAAAACQAAAAAAAAAAgABAADyBSoBAAAAjwAAAAAAAAAAAAAAEAAHAAAAoEEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIABQABAAAAAAAAAAIAAAAAAHBBgAELJwEAAAAA8gUqAQAAAAEAAAAAAAAAKQAAAAAAAAAAAHBCAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAoCAABhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhAAAAAAAAAAA="} diff --git a/mapd_repo/cereal/wire_compatibility_test.go b/mapd_repo/cereal/wire_compatibility_test.go index 58320a5d42..9df5bfc358 100644 --- a/mapd_repo/cereal/wire_compatibility_test.go +++ b/mapd_repo/cereal/wire_compatibility_test.go @@ -1,7 +1,6 @@ package cereal import ( - "bytes" "capnproto.org/go/capnp/v3" "encoding/base64" "encoding/json" @@ -61,7 +60,58 @@ func TestMapdV1HostWireFixture(t *testing.T) { if err != nil { t.Fatal(err) } - if !bytes.Equal(raw, expected) { + actualMessage, err := capnp.Unmarshal(raw) + if err != nil { + t.Fatal(err) + } + oldMessage, err := capnp.Unmarshal(expected) + if err != nil { + t.Fatal(err) + } + actualRoot, err := actualMessage.Root() + if err != nil { + t.Fatal(err) + } + oldRoot, err := oldMessage.Root() + if err != nil { + t.Fatal(err) + } + // Appended default fields can enlarge storage without changing old values. + equal, err := capnp.Equal(actualRoot, oldRoot) + if err != nil || !equal { t.Fatal("provider wire differs from shared host fixture; review both schemas") } } + +func TestPreviewHostWireFixture(t *testing.T) { + data, err := os.ReadFile("testdata/mapd_preview_wire.json") + if err != nil { + t.Fatal(err) + } + var fixture struct { + EventBase64 string `json:"event_base64"` + } + if err := json.Unmarshal(data, &fixture); err != nil { + t.Fatal(err) + } + raw, err := base64.StdEncoding.DecodeString(fixture.EventBase64) + if err != nil { + t.Fatal(err) + } + msg, err := capnp.Unmarshal(raw) + if err != nil { + t.Fatal(err) + } + event, err := log.ReadRootEvent(msg) + if err != nil { + t.Fatal(err) + } + out, err := event.MapdOut() + if err != nil { + t.Fatal(err) + } + boundary, err := out.PreviewBoundaryId() + if err != nil || !event.Valid() || out.SampleVersion() != 2 || out.SpeedLimit() != 20 || out.PreviewSpeedLimit() != 15 || out.PreviewDistance() != 60 || len(boundary) != 64 || out.PreviewWayId() != 2 { + t.Fatal("provider preview differs from shared host fixture") + } +} diff --git a/mapd_repo/limit_preview.go b/mapd_repo/limit_preview.go new file mode 100644 index 0000000000..9e9c0a0106 --- /dev/null +++ b/mapd_repo/limit_preview.go @@ -0,0 +1,82 @@ +package main + +import ( + "crypto/sha256" + "fmt" + "math" + + "pfeifer.dev/mapd/cereal/custom" + "pfeifer.dev/mapd/maps" +) + +const previewHorizonM = 500 +const previewMaxWays = 64 + +type LimitPreview struct { + Speed float32 + Distance float32 + BoundaryID string + WayID int64 +} + +// A preview follows only a unique legal continuation, never a guessed turn. +func (s *State) previewLimit() LimitPreview { + if !s.ShadowOnly || s.SnapshotID == "" || s.CurrentWay.SelectionType != custom.WaySelectionType_current || s.MatchedLimitMps <= 0 { + return LimitPreview{} + } + way, forward, position := s.CurrentWay.Way, s.CurrentWay.OnWay.IsForward, s.Position + distance := float32(0) + seen := map[int64]bool{way.Id(): true} + for range previewMaxWays { + _, end := way.GetStartEnd(forward) + if distance+position.DistanceTo(end) > previewHorizonM { + return LimitPreview{} + } + segment, found, err := way.DistanceToNode(position, forward, end) + if err != nil || !found || segment < 0 || math.IsNaN(float64(segment)) || math.IsInf(float64(segment), 0) { + return LimitPreview{} + } + distance += segment + box := s.Data.OverlapBox() + if distance <= 0 || distance > previewHorizonM || !box.PosInside(end) { + return LimitPreview{} + } + matches, err := way.MatchingWays(&s.Data, end) + if err != nil { + return LimitPreview{} + } + legal := map[int64]maps.Way{} + for _, next := range matches { + first, last := next.GetStartEnd(true) + if first.Equals(last) && !next.OneWay() { + return LimitPreview{} // A two-way loop has two possible entry directions. + } + if next.Id() > 0 && (next.IsForwardFrom(end) || !next.OneWay()) { + legal[next.Id()] = next + } + } + if len(legal) != 1 { + return LimitPreview{} + } + var next maps.Way + for _, candidate := range legal { + next = candidate + } + if seen[next.Id()] { + return LimitPreview{} + } + nextForward := next.IsForwardFrom(end) + road := CurrentWay{Way: next, OnWay: maps.OnWayResult{IsForward: nextForward}} + speed := road.EffectiveMaxSpeed() + if speed <= 0 || math.IsNaN(speed) || math.IsInf(speed, 0) || speed > math.MaxFloat32 { + return LimitPreview{} + } + if math.Abs(speed-float64(s.MatchedLimitMps)) > .02 { + key := fmt.Sprintf("%s:%d:%d:%t:%.7f:%.7f", s.SnapshotID, way.Id(), next.Id(), nextForward, end.Lat(), end.Lon()) + return LimitPreview{float32(speed), distance, fmt.Sprintf("%x", sha256.Sum256([]byte(key))), next.Id()} + } + seen[next.Id()] = true + way, forward, position = next, nextForward, end + } + return LimitPreview{} +} diff --git a/mapd_repo/limit_preview_test.go b/mapd_repo/limit_preview_test.go new file mode 100644 index 0000000000..b2f8434796 --- /dev/null +++ b/mapd_repo/limit_preview_test.go @@ -0,0 +1,200 @@ +package main + +import ( + "crypto/sha256" + "fmt" + "math" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "capnproto.org/go/capnp/v3" + "pfeifer.dev/mapd/cereal" + "pfeifer.dev/mapd/cereal/custom" + "pfeifer.dev/mapd/cereal/offline" + "pfeifer.dev/mapd/maps" + m "pfeifer.dev/mapd/math" +) + +func previewState(t *testing.T, specs []syntheticWay, index int, forward bool, latitude float64) State { + t.Helper() + tile := packedRoadTile(t, specs) + return State{ShadowOnly: true, SnapshotID: "verified-snapshot", Data: tile, + Position: m.NewPosition(latitude, -97.9), MatchedLimitMps: float32(specs[index].speed), + CurrentWay: CurrentWay{Way: tile.Ways.At(index), OnWay: maps.OnWayResult{IsForward: forward}, SelectionType: custom.WaySelectionType_current}} +} + +func previewRoads() []syntheticWay { + return []syntheticWay{ + {1, 20, 35.1, -97.9, 35.101, -97.9, false, 2}, + {2, 20, 35.101, -97.9, 35.102, -97.9, false, 2}, + {3, 10, 35.102, -97.9, 35.103, -97.9, false, 2}, + } +} + +func TestPreviewRawDistanceAndStableBoundaryAcrossApproachWays(t *testing.T) { + s := previewState(t, previewRoads(), 0, true, 35.1005) + a := s.previewLimit() + s = previewState(t, previewRoads(), 1, true, 35.1015) + b := s.previewLimit() + if a.Speed != 10 || a.WayID != 3 || a.BoundaryID == "" || a.BoundaryID != b.BoundaryID || math.Abs(float64(a.Distance-b.Distance)-111.2) > 1 { + t.Fatalf("unexpected road preview: %+v %+v", a, b) + } + s.Position = m.NewPosition(35.1014, -97.9) + if s.previewLimit().Distance <= b.Distance { + t.Fatal("raw distance was held at a previous minimum") + } + s.SnapshotID = "changed-snapshot" + if s.previewLimit().BoundaryID == b.BoundaryID { + t.Fatal("boundary identity survived a different snapshot") + } +} + +func TestPreviewDirectionUnknownLimitsBranchesAndHorizon(t *testing.T) { + s := previewState(t, previewRoads(), 2, false, 35.1025) + if p := s.previewLimit(); p.Speed != 20 || p.WayID != 2 || p.Distance <= 0 { + t.Fatalf("reverse traversal failed: %+v", p) + } + for _, mode := range []string{"branch", "unknown", "oneway", "horizon", "unqualified", "unverified"} { + t.Run(mode, func(t *testing.T) { + roads := previewRoads() + index, forward, latitude := 0, true, 35.1005 + switch mode { + case "branch": + roads = append(roads, syntheticWay{4, 30, 35.101, -97.9, 35.102, -97.899, false, 2}) + case "unknown": + roads[1].speed = 0 + case "oneway": + roads[1].oneWay = true + index, forward, latitude = 2, false, 35.1025 + case "horizon": + roads[0].lat1 = 35.09 + latitude = 35.091 + } + s := previewState(t, roads, index, forward, latitude) + if mode == "unqualified" { + s.CurrentWay.SelectionType = custom.WaySelectionType_possible + } + if mode == "unverified" { + s.SnapshotID = "" + } + if p := s.previewLimit(); p != (LimitPreview{}) { + t.Fatalf("uncertain preview escaped: %+v", p) + } + }) + } +} + +func TestPreviewClosedWayRequiresOneLegalDirection(t *testing.T) { + for _, tc := range []struct { + name string + oneWay, branch, wantPreview bool + }{ + {"one-way loop", true, false, true}, + {"two-way loop", false, false, false}, + {"two-way loop plus continuation", false, true, false}, + } { + t.Run(tc.name, func(t *testing.T) { + roads := []syntheticWay{ + {1, 20, 35.1, -97.9, 35.101, -97.9, true, 2}, + {2, 10, 35.101, -97.9, 35.102, -97.9, tc.oneWay, 2}, + } + if tc.branch { + roads = append(roads, syntheticWay{3, 15, 35.101, -97.9, 35.102, -97.899, false, 2}) + } + decoded, err := capnp.UnmarshalPacked(packedRoadTileBytes(t, roads)) + if err != nil { + t.Fatal(err) + } + root, err := decoded.Root() + if err != nil { + t.Fatal(err) + } + msg, _, err := capnp.NewMessage(capnp.SingleSegment(nil)) + if err != nil { + t.Fatal(err) + } + if err := msg.SetRoot(root); err != nil { + t.Fatal(err) + } + raw, err := offline.ReadRootOffline(msg) + if err != nil { + t.Fatal(err) + } + ways, err := raw.Ways() + if err != nil { + t.Fatal(err) + } + loop := ways.At(1) + loop.SetMaxLon(-97.898) + nodes, err := loop.NewNodes(4) + if err != nil { + t.Fatal(err) + } + for i, p := range [][2]float64{{35.101, -97.9}, {35.102, -97.9}, {35.102, -97.899}, {35.101, -97.9}} { + nodes.At(i).SetLatitude(p[0]) + nodes.At(i).SetLongitude(p[1]) + } + data, err := msg.MarshalPacked() + if err != nil { + t.Fatal(err) + } + tile := maps.ReadOffline(data) + if !tile.Loaded { + t.Fatal("closed road tile rejected") + } + s := State{ShadowOnly: true, SnapshotID: "verified-snapshot", Data: tile, + Position: m.NewPosition(35.1005, -97.9), MatchedLimitMps: 20, + CurrentWay: CurrentWay{Way: tile.Ways.At(0), OnWay: maps.OnWayResult{IsForward: true}, SelectionType: custom.WaySelectionType_current}} + p := s.previewLimit() + if tc.wantPreview { + if p.Speed != 10 || p.WayID != 2 || p.Distance <= 0 || p.BoundaryID == "" { + t.Fatalf("legal loop preview missing: %+v", p) + } + } else if p != (LimitPreview{}) { + t.Fatalf("ambiguous loop produced a preview: %+v", p) + } + }) + } +} + +func TestShadowPreviewSharesFixFreshnessWithoutControlOutputs(t *testing.T) { + roads := []syntheticWay{{1, 20, 35.149, -97.9, 35.151, -97.9, false, 2}, {2, 10, 35.151, -97.9, 35.153, -97.9, false, 2}} + data := packedRoadTileBytes(t, roads) + id := strings.Repeat("a", 64) + dir := filepath.Join(t.TempDir(), id) + area := maps.Area{Box: m.Box{MinPos: m.NewPosition(35, -98), MaxPos: m.NewPosition(35.25, -97.75)}} + path := maps.GenerateBoundsFileName(area, maps.OfflineSettings{OutputDirectory: dir}) + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, data, 0644); err != nil { + t.Fatal(err) + } + rel, err := filepath.Rel(dir, path) + if err != nil { + t.Fatal(err) + } + load := maps.SnapshotLoader(dir, maps.SnapshotReceipt{Tiles: []maps.SnapshotTile{{Path: rel, Size: int64(len(data)), SHA256: fmt.Sprintf("%x", sha256.Sum256(data))}}}) + s := State{ShadowOnly: true, SnapshotID: id} + s.Init() + boot := uint64(10_100_000_000) + s.bootNow = func() uint64 { return boot } + sample := syntheticSample(t, cereal.GpsSourceExternal, 10_000_000_000, 1, true, true) + s.ProcessGps(sample, true, load, time.Unix(0, 0)) + sample.SourceChanged, sample.FixMonoTime = false, 10_050_000_000 + s.ProcessGps(sample, true, load, time.Unix(0, 1)) + event, output := decodedOutput(t, &s) + boundary, _ := output.PreviewBoundaryId() + if !event.Valid() || output.SpeedLimit() != 20 || output.PreviewSpeedLimit() != 10 || boundary == "" || output.PreviewDistance() <= 0 || output.NextSpeedLimit() != 0 || output.SuggestedSpeed() != 0 { + t.Fatalf("shadow preview: valid=%v status=%v way=%v limit=%v preview=%+v", event.Valid(), output.RoadStatus(), output.WaySelectionType(), output.SpeedLimit(), s.LimitPreview) + } + boot = 10_600_000_000 + event, output = decodedOutput(t, &s) + boundary, _ = output.PreviewBoundaryId() + if event.Valid() || output.PreviewSpeedLimit() != 0 || boundary != "" { + t.Fatal("expired GPS retained an upcoming limit") + } +} diff --git a/mapd_repo/maps/generate_offline.go b/mapd_repo/maps/generate_offline.go index 39e87fd1ae..1ce045b9bc 100644 --- a/mapd_repo/maps/generate_offline.go +++ b/mapd_repo/maps/generate_offline.go @@ -11,6 +11,7 @@ import ( "os" "reflect" "runtime" + "slices" "strconv" "strings" "syscall" @@ -162,7 +163,6 @@ func GenerateOffline(s OfflineSettings) error { MaxSpeedBackward: ParseMaxSpeed(tags["maxspeed:backward"]), MaxSpeedAdvisory: ParseMaxSpeed(tags["maxspeed:advisory"]), Lanes: uint8(lanes), - OneWay: tags["oneway"] == "yes", Id: int64(way.ID), HighwayClass: HighwayClassFromTag(tags["highway"]), @@ -191,6 +191,17 @@ func GenerateOffline(s OfflineSettings) error { tmpWay.Nodes[i].Latitude = n.Lat tmpWay.Nodes[i].Longitude = n.Lon } + switch tags["oneway"] { + case "yes", "1": + tmpWay.OneWay = true + case "-1": + tmpWay.OneWay = true + slices.Reverse(tmpWay.Nodes) + tmpWay.MaxSpeedForward, tmpWay.MaxSpeedBackward = tmpWay.MaxSpeedBackward, tmpWay.MaxSpeedForward + tmpWay.MaxSpeedForwardConditional, tmpWay.MaxSpeedBackwardConditional = tmpWay.MaxSpeedBackwardConditional, tmpWay.MaxSpeedForwardConditional + case "": + tmpWay.OneWay = tags["junction"] == "roundabout" || tags["highway"] == "motorway" + } tmpWay.Box.MinPos = m.NewPosition(minLat, minLon) tmpWay.Box.MaxPos = m.NewPosition(maxLat, maxLon) scannedWays = append(scannedWays, tmpWay) diff --git a/mapd_repo/maps/generate_offline_test.go b/mapd_repo/maps/generate_offline_test.go new file mode 100644 index 0000000000..1b3385684e --- /dev/null +++ b/mapd_repo/maps/generate_offline_test.go @@ -0,0 +1,78 @@ +package maps + +import ( + "math" + "os" + "path/filepath" + "testing" + + m "pfeifer.dev/mapd/math" +) + +func TestGeneratorPreservesFixedOneWayDirectionAndSpeedTags(t *testing.T) { + for _, tc := range []struct { + name, oneWay, highway, junction string + wantOneWay, reverse bool + }{ + {"ordinary", "", "residential", "", false, false}, + {"yes", "yes", "residential", "", true, false}, + {"numeric", "1", "residential", "", true, false}, + {"reverse", "-1", "residential", "", true, true}, + {"roundabout", "", "residential", "roundabout", true, false}, + {"motorway", "", "motorway", "", true, false}, + {"roundabout explicit no", "no", "residential", "roundabout", false, false}, + {"roundabout explicit zero", "0", "residential", "roundabout", false, false}, + {"motorway explicit no", "no", "motorway", "", false, false}, + {"motorway explicit zero", "0", "motorway", "", false, false}, + {"motorway link", "", "motorway_link", "", false, false}, + {"reversible", "reversible", "residential", "roundabout", false, false}, + {"alternating", "alternating", "motorway", "", false, false}, + } { + t.Run(tc.name, func(t *testing.T) { + forwardRule, backwardRule := "15 @ (Mo-Fr 08:00-09:00)", "25 @ (Sa 10:00-11:00)" + tags := []string{"highway", tc.highway, "maxspeed", "30 mph", + "maxspeed:forward", "10", "maxspeed:backward", "20", + "maxspeed:forward:conditional", forwardRule, "maxspeed:backward:conditional", backwardRule} + if tc.oneWay != "" { + tags = append(tags, "oneway", tc.oneWay) + } + if tc.junction != "" { + tags = append(tags, "junction", tc.junction) + } + root := t.TempDir() + pbf := filepath.Join(root, "road.osm.pbf") + if err := os.WriteFile(pbf, syntheticPBF(tags...), 0600); err != nil { + t.Fatal(err) + } + output := filepath.Join(root, "tiles") + if err := GenerateOffline(OfflineSettings{InputFile: pbf, OutputDirectory: output, Overlap: .001, + Box: m.Box{MinPos: m.NewPosition(35, -98), MaxPos: m.NewPosition(35.25, -97.75)}}); err != nil { + t.Fatal(err) + } + tile, err := FindWaysAroundPositionIn(m.NewPosition(35.15, -97.9), output) + if err != nil || !tile.Loaded || tile.Ways.Len() != 1 { + t.Fatalf("generated road not loaded: %v", err) + } + way := tile.Ways.At(0) + if way.Nodes.Len() != 2 { + t.Fatalf("generated road has %d nodes", way.Nodes.Len()) + } + first, last := 35.1, 35.2 + forwardSpeed, backwardSpeed := 10./3.6, 20./3.6 + if tc.reverse { + first, last = last, first + forwardSpeed, backwardSpeed = backwardSpeed, forwardSpeed + forwardRule, backwardRule = backwardRule, forwardRule + } + firstNode, lastNode := way.Nodes.At(0), way.Nodes.At(1) + if way.Id() != 42 || way.OneWay() != tc.wantOneWay || + math.Abs(firstNode.Lat()-first) > 1e-9 || math.Abs(lastNode.Lat()-last) > 1e-9 || + math.Abs(way.MaxSpeed()-13.4112) > 1e-6 || + math.Abs(way.MaxSpeedForward()-forwardSpeed) > 1e-5 || math.Abs(way.MaxSpeedBackward()-backwardSpeed) > 1e-5 || + way.ConditionalMaxSpeedRaw(true) != forwardRule || way.ConditionalMaxSpeedRaw(false) != backwardRule { + t.Fatalf("generated road lost direction or speed metadata: oneWay=%v forward=%v backward=%v", + way.OneWay(), way.MaxSpeedForward(), way.MaxSpeedBackward()) + } + }) + } +} diff --git a/mapd_repo/maps/snapshot_test.go b/mapd_repo/maps/snapshot_test.go index a9a19c1a6a..b5e6a79f1b 100644 --- a/mapd_repo/maps/snapshot_test.go +++ b/mapd_repo/maps/snapshot_test.go @@ -309,17 +309,25 @@ func pbfBlock(kind string, raw []byte) []byte { data = append(data, header...) return append(data, blob...) } -func syntheticPBF() []byte { +func syntheticPBF(tags ...string) []byte { header := pbfBytesField(nil, 4, []byte("OsmSchema-V0.6")) header = pbfBytesField(header, 5, []byte("LocationsOnWays")) + if len(tags) == 0 { + tags = []string{"highway", "residential", "maxspeed", "30 mph"} + } var table []byte - for _, value := range []string{"", "highway", "residential", "maxspeed", "30 mph"} { + for _, value := range append([]string{""}, tags...) { table = pbfBytesField(table, 1, []byte(value)) } + var keys, values []uint64 + for i := 0; i < len(tags); i += 2 { + keys = append(keys, uint64(i+1)) + values = append(values, uint64(i+2)) + } var way []byte way = pbfVarintField(way, 1, 42) - way = pbfBytesField(way, 2, pbfPacked(1, 3)) - way = pbfBytesField(way, 3, pbfPacked(2, 4)) + way = pbfBytesField(way, 2, pbfPacked(keys...)) + way = pbfBytesField(way, 3, pbfPacked(values...)) way = pbfBytesField(way, 8, pbfPacked(protowire.EncodeZigZag(1), protowire.EncodeZigZag(1))) way = pbfBytesField(way, 9, pbfPacked(protowire.EncodeZigZag(351000000), protowire.EncodeZigZag(1000000))) way = pbfBytesField(way, 10, pbfPacked(protowire.EncodeZigZag(-979000000), protowire.EncodeZigZag(0))) diff --git a/mapd_repo/maps/way.go b/mapd_repo/maps/way.go index 4f367bbbea..910c9656f0 100644 --- a/mapd_repo/maps/way.go +++ b/mapd_repo/maps/way.go @@ -149,8 +149,12 @@ func (w *Way) IsForwardFrom(matchNode m.Position) bool { return true } - lastNode := w.Nodes.At(w.Nodes.Len()-1) - return !lastNode.Equals(matchNode) + lastNode := w.Nodes.At(w.Nodes.Len() - 1) + if lastNode.Equals(matchNode) { + // A closed one-way can be entered at its repeated endpoint in stored order. + return w.OneWay() && matchNode.Equals(w.Nodes.At(0)) + } + return true } func IsForward(lineStart m.Position, lineEnd m.Position, bearing float64) bool { @@ -431,7 +435,7 @@ func (w *Way) DistanceFrom(pos m.Position) (DistanceResult, error) { minIdx := 0 for i := 0; i < w.Nodes.Len()-1; i++ { nodeStart := w.Nodes.At(i) - nodeEnd := w.Nodes.At(i+1) + nodeEnd := w.Nodes.At(i + 1) line := m.Line{Start: nodeStart, End: nodeEnd} linePosition := line.NearestPosition(pos) distance := pos.DistanceTo(linePosition.Pos) @@ -447,7 +451,7 @@ func (w *Way) DistanceFrom(pos m.Position) (DistanceResult, error) { onWayDistance := minNodeStart.DistanceTo(minLinePosition.Pos) for i := range minIdx { nodeStart := w.Nodes.At(i) - nodeEnd := w.Nodes.At(i+1) + nodeEnd := w.Nodes.At(i + 1) onWayDistance += nodeStart.DistanceTo(nodeEnd) } @@ -491,8 +495,7 @@ func (w *Way) MatchingWays(offlineMaps *Offline, matchNode m.Position) ([]Way, e continue } - box := way.Box() - if box.Equals(w.Box()) { + if way.Id() == w.Id() { continue } diff --git a/mapd_repo/maps/way_geometry_test.go b/mapd_repo/maps/way_geometry_test.go index c6aa95866f..464eed056e 100644 --- a/mapd_repo/maps/way_geometry_test.go +++ b/mapd_repo/maps/way_geometry_test.go @@ -55,3 +55,48 @@ func TestBearingAlignmentIsAnUndirectedNonnegativePenalty(t *testing.T) { } } } + +func TestWayEntryDirectionForOpenAndClosedWays(t *testing.T) { + start, end := m.NewPosition(35.15, -97.9), m.NewPosition(35.151, -97.9) + for _, tc := range []struct { + name string + closed, oneWay, fromEnd, wantForward bool + }{ + {"open start", false, true, false, true}, + {"open end", false, true, true, false}, + {"closed one-way", true, true, false, true}, + {"closed two-way", true, false, false, false}, + } { + t.Run(tc.name, func(t *testing.T) { + _, seg, err := capnp.NewMessage(capnp.SingleSegment(nil)) + if err != nil { + t.Fatal(err) + } + raw, err := offline.NewRootWay(seg) + if err != nil { + t.Fatal(err) + } + raw.SetOneWay(tc.oneWay) + positions := []m.Position{start, end} + if tc.closed { + positions = append(positions, start) + } + nodes, err := raw.NewNodes(int32(len(positions))) + if err != nil { + t.Fatal(err) + } + for i, p := range positions { + nodes.At(i).SetLatitude(p.Lat()) + nodes.At(i).SetLongitude(p.Lon()) + } + from := start + if tc.fromEnd { + from = end + } + way := NewWay(raw) + if got := way.IsForwardFrom(from); got != tc.wantForward { + t.Fatalf("entry direction: got %v want %v", got, tc.wantForward) + } + }) + } +} diff --git a/mapd_repo/road_ambiguity_test.go b/mapd_repo/road_ambiguity_test.go index 298c201364..e70d04eca1 100644 --- a/mapd_repo/road_ambiguity_test.go +++ b/mapd_repo/road_ambiguity_test.go @@ -24,7 +24,7 @@ type syntheticWay struct { // The actual offline Capnp representation is packed, decoded, and passed to // State; tests do not mock GetCurrentWay or BuildMessage. -func packedRoadTile(tb testing.TB, specs []syntheticWay) maps.Offline { +func packedRoadTileBytes(tb testing.TB, specs []syntheticWay) []byte { tb.Helper() msg, seg, err := capnp.NewMessage(capnp.SingleSegment(nil)) if err != nil { @@ -65,6 +65,12 @@ func packedRoadTile(tb testing.TB, specs []syntheticWay) maps.Offline { if err != nil { tb.Fatal(err) } + return data +} + +func packedRoadTile(tb testing.TB, specs []syntheticWay) maps.Offline { + tb.Helper() + data := packedRoadTileBytes(tb, specs) parsed := maps.ReadOffline(data) if !parsed.Loaded { tb.Fatal("packed tile rejected") diff --git a/mapd_repo/state.go b/mapd_repo/state.go index d6f3340379..2f095a79de 100644 --- a/mapd_repo/state.go +++ b/mapd_repo/state.go @@ -46,6 +46,7 @@ type State struct { SourceGeneration uint64 ProducerSession uint64 MatchedLimitMps float32 + LimitPreview LimitPreview lastMapLoadAttempt time.Time bootNow func() uint64 // The fix the current road result was computed from. The loop runs at @@ -90,6 +91,7 @@ func (s *State) clearRoadDerived() { s.TargetVelocities = nil s.MapCurveSpeed = 0 s.MatchedLimitMps = 0 + s.LimitPreview = LimitPreview{} s.NextHazard.Reset() s.NextAdvisorySpeed.Reset() s.clearSpeedLimitEvidence() @@ -255,6 +257,7 @@ func (s *State) ProcessGps(sample cereal.GpsSample, success bool, load MapLoader s.MatchedLimitMps = float32(limit) s.RoadStatus = custom.MapdOut_SampleStatus_matchedLimit } + s.LimitPreview = s.previewLimit() } func (s *State) SuggestedSpeed() float32 { @@ -361,6 +364,12 @@ func (s *State) buildMessageAt(now uint64) (*capnp.Message, error) { output.SetRoadName(s.CurrentWay.Way.Name()) output.SetSpeedLimit(s.MatchedLimitMps) + if s.LimitPreview.BoundaryID != "" { + output.SetPreviewSpeedLimit(s.LimitPreview.Speed) + output.SetPreviewDistance(s.LimitPreview.Distance) + output.SetPreviewBoundaryId(s.LimitPreview.BoundaryID) + output.SetPreviewWayId(s.LimitPreview.WayID) + } output.SetConditionalSpeedLimit(s.CurrentWay.ConditionalMaxSpeedRaw()) diff --git a/openpilot/cereal/custom.capnp b/openpilot/cereal/custom.capnp index 0c1cc6a3ce..02a25798a1 100644 --- a/openpilot/cereal/custom.capnp +++ b/openpilot/cereal/custom.capnp @@ -1157,6 +1157,11 @@ struct MapdOut @0xa4f1eb3323f5f582 { computedMonoTime @31 :UInt64; # Computation boot-time nanoseconds, not send time. sourceGeneration @32 :UInt64; producerSession @33 :UInt64; # Random nonzero process session. + # Data-only preview from the same qualified GPS fix; no controller smoothing. + previewSpeedLimit @34 :Float32; + previewDistance @35 :Float32; + previewBoundaryId @36 :Text; + previewWayId @37 :Int64; enum SampleStatus { unknown @0; diff --git a/openpilot/starpilot/maps/provider/manifest.json b/openpilot/starpilot/maps/provider/manifest.json index 590e7c2ce7..cbb4a7bc28 100644 --- a/openpilot/starpilot/maps/provider/manifest.json +++ b/openpilot/starpilot/maps/provider/manifest.json @@ -1 +1 @@ -{"binarySha256":"327883964b0cbde92dd6c6ae03670495dd97d57869c806bb64cfbcfb73d96552","goVersion":"go1.25.1","schemaVersion":1,"sourceDigest":"8a77333de0bb02a9b3acd151a662cacaa078dbea36c1de62da790cff8ab1549c","sourceRevision":"127ddb75b6eb2861ce6327e42e12da80b318dd91","target":"linux-arm64-static","upstreamRevision":"7201c6b4b4ec1b0b9ea21daa8c05b80fdd7e01ee"} +{"binarySha256":"7d7c9fb80b86cf6fb802e3d5718f75de026af8f8363088fc1268f79f9dbbc7e2","goVersion":"go1.25.1","schemaVersion":1,"sourceDigest":"7d315a4bfba3c26b9c88366bf2248338f4d3a9e0997d460909bb067c59627a6e","sourceRevision":"87b8b2ada572b3d96cbe2e7507da7d741dd35fc8","target":"linux-arm64-static","upstreamRevision":"7201c6b4b4ec1b0b9ea21daa8c05b80fdd7e01ee"} diff --git a/openpilot/starpilot/maps/provider/mapd b/openpilot/starpilot/maps/provider/mapd index 1bdac855d6..202195d64a 100755 Binary files a/openpilot/starpilot/maps/provider/mapd and b/openpilot/starpilot/maps/provider/mapd differ diff --git a/tools/vendor/README.md b/tools/vendor/README.md index 6491e60cf0..a73a25f0ca 100644 --- a/tools/vendor/README.md +++ b/tools/vendor/README.md @@ -35,3 +35,34 @@ checks reject that layout. Dependency provenance checks do not replace runtime, firmware, or vehicle qualification for an update. + +## Local Mapd integration + +`mapd_repo/` contains tracked source with StarPilot changes. The static Linux +ARM64 executable and its source/hash manifest are also tracked, under +`openpilot/starpilot/maps/provider/`. The upstream pin identifies the original +merge base; it does not require this source folder to match upstream exactly. + +The local `limit_preview.go` helper publishes bounded, data-only speed-limit +previews from the provider's qualified road evidence. Python SLC owns advance +timing, offsets, confirmations, overrides and vehicle commands. Preserve the +preview lifecycle hooks, matching/schema changes and Go/Python regression tests +together when updating the provider. + +Fixed one-way generation and closed-loop entry are backported from +[pfeiferj/mapd PR #105](https://github.com/pfeiferj/mapd/pull/105), merged as +`92a22aaac0396330d0db40432407f011ff723553`. Reverse one-way roads reverse their +nodes and both directional speed-tag pairs. Closed two-way loops cannot qualify +an early preview because their entry direction is ambiguous. Generator changes +apply to newly generated tiles; existing downloaded tiles are not rewritten. + +Use the sync command above with dependency `mapd_repo` for upstream updates. +It preserves committed local changes through a three-way merge and stops on +conflicts; a normal update does not require manually reapplying this feature. +Review the merged behavior and run the provider and host SLC/wire tests. Follow +`mapd_repo/INTEGRATION.md` if changed schemas require regenerating Go bindings. + +After changing provider source or its upstream pin, run `./build --mapd`, then +`python3 tools/release/stage_mapd_provider.py --source . --require-tracked`. +Commit the source, bindings, tests, executable and manifest together. Existing +package checks reject a binary whose recorded source or upstream pin is stale.