@@ -2,6 +2,8 @@ package main
22
33import (
44 "context"
5+ _ "embed"
6+ "encoding/json"
57 "math"
68 "math/rand"
79 "sort"
@@ -39,6 +41,41 @@ type demoCharge struct {
3941 category string
4042 title string
4143 pt []float64
44+ curve [][]float64 // optional [soc, kw]; synthesized when empty
45+ }
46+
47+ // demoTripJSON is a real multi-leg road trip donated to the demo dataset:
48+ // GPS traces, cities, and charging curves are genuine, but it is anchored to
49+ // a synthetic date and contains no home location.
50+ //
51+ //go:embed demo_trip.json
52+ var demoTripJSON []byte
53+
54+ type demoTrip struct {
55+ Drives []struct {
56+ OffsetMin int `json:"offset_min"`
57+ Km float64 `json:"km"`
58+ DurMin int `json:"dur_min"`
59+ SpeedMax float64 `json:"speed_max"`
60+ From string `json:"from"`
61+ To string `json:"to"`
62+ S0 int `json:"s0"`
63+ S1 int `json:"s1"`
64+ KWh float64 `json:"kwh"`
65+ Coords [][]float64 `json:"coords"`
66+ } `json:"drives"`
67+ Charges []struct {
68+ OffsetMin int `json:"offset_min"`
69+ KWh float64 `json:"kwh"`
70+ DurMin int `json:"dur_min"`
71+ S0 int `json:"s0"`
72+ S1 int `json:"s1"`
73+ PeakKw float64 `json:"peak_kw"`
74+ Name string `json:"name"`
75+ Lng float64 `json:"lng"`
76+ Lat float64 `json:"lat"`
77+ Curve [][]float64 `json:"curve"`
78+ } `json:"charges"`
4279}
4380
4481// Centre of the synthetic world (a neutral land point used only for demo
@@ -134,9 +171,37 @@ func newDemoStore(units string) *demoStore {
134171 addDrive (day - 1 , 10 , lake , hills )
135172 addDrive (day - 1 , 16 , hills , home )
136173 }
174+ s .addFeaturedTrip (now )
137175 return s
138176}
139177
178+ // addFeaturedTrip loads the embedded real road trip, anchored so it departs
179+ // five days ago in the evening. IDs are fixed (drives 1500+, charges 2500+)
180+ // so the trip can be shared with a stable ?sel= URL.
181+ func (s * demoStore ) addFeaturedTrip (now time.Time ) {
182+ var trip demoTrip
183+ if err := json .Unmarshal (demoTripJSON , & trip ); err != nil {
184+ return
185+ }
186+ day := now .AddDate (0 , 0 , - 5 )
187+ anchor := time .Date (day .Year (), day .Month (), day .Day (), 21 , 10 , 0 , 0 , now .Location ())
188+ for i , d := range trip .Drives {
189+ s .drives = append (s .drives , demoDrive {
190+ id : 1500 + i , start : anchor .Add (time .Duration (d .OffsetMin ) * time .Minute ),
191+ km : d .Km , durMin : d .DurMin , speedMax : d .SpeedMax ,
192+ from : d .From , to : d .To , s0 : d .S0 , s1 : d .S1 , kwh : d .KWh , coords : d .Coords ,
193+ })
194+ }
195+ for i , c := range trip .Charges {
196+ s .charges = append (s .charges , demoCharge {
197+ id : 2500 + i , start : anchor .Add (time .Duration (c .OffsetMin ) * time .Minute ),
198+ kwh : c .KWh , durMin : c .DurMin , s0 : c .S0 , s1 : c .S1 , peakKw : c .PeakKw ,
199+ category : "supercharger" , title : c .Name ,
200+ pt : []float64 {c .Lng , c .Lat }, curve : c .Curve ,
201+ })
202+ }
203+ }
204+
140205func (s * demoStore ) Cars (ctx context.Context ) ([]Car , error ) {
141206 return []Car {{ID : 1 , Name : "Demo" , Model : "Model Y" }}, nil
142207}
@@ -297,22 +362,33 @@ func (s *demoStore) Detail(ctx context.Context, id string) (*Detail, error) {
297362 continue
298363 }
299364 det := & Detail {Activity : s .chargeActivity (c )}
300- // Plausible charging taper: flat to ~42% SoC, declining after.
301365 sum , minKw := 0.0 , c .peakKw
302- steps := max (c .s1 - c . s0 , 1 )
303- for i := 0 ; i <= steps ; i ++ {
304- soc := float64 ( c . s0 + i )
305- f := 1.0
306- if soc < 15 {
307- f = 0.5 + 0.033 * soc
308- } else if soc > 42 {
309- f = math . Max ( 0.1 , 1 - ( soc - 42 ) * 0.017 )
366+ if len (c .curve ) > 0 {
367+ // Real recorded curve embedded with the featured trip.
368+ for _ , p := range c . curve {
369+ det . Curve = append ( det . Curve , CurvePoint { Soc : p [ 0 ], Kw : p [ 1 ]})
370+ sum += p [ 1 ]
371+ if p [ 1 ] < minKw {
372+ minKw = p [ 1 ]
373+ }
310374 }
311- kw := math .Round (c .peakKw * f )
312- det .Curve = append (det .Curve , CurvePoint {Soc : soc , Kw : kw })
313- sum += kw
314- if kw < minKw {
315- minKw = kw
375+ } else {
376+ // Plausible charging taper: flat to ~42% SoC, declining after.
377+ steps := max (c .s1 - c .s0 , 1 )
378+ for i := 0 ; i <= steps ; i ++ {
379+ soc := float64 (c .s0 + i )
380+ f := 1.0
381+ if soc < 15 {
382+ f = 0.5 + 0.033 * soc
383+ } else if soc > 42 {
384+ f = math .Max (0.1 , 1 - (soc - 42 )* 0.017 )
385+ }
386+ kw := math .Round (c .peakKw * f )
387+ det .Curve = append (det .Curve , CurvePoint {Soc : soc , Kw : kw })
388+ sum += kw
389+ if kw < minKw {
390+ minKw = kw
391+ }
316392 }
317393 }
318394 det .AvgKw = math .Round (sum / float64 (len (det .Curve )))
0 commit comments