Commit
+253 -55 +/-5 browse
1 | diff --git a/encoding.go b/encoding.go |
2 | new file mode 100644 |
3 | index 0000000..bdc2597 |
4 | --- /dev/null |
5 | +++ b/encoding.go |
6 | @@ -0,0 +1,41 @@ |
7 | + package main |
8 | + |
9 | + import ( |
10 | + "bytes" |
11 | + "io" |
12 | + |
13 | + "gonum.org/v1/gonum/graph" |
14 | + "gonum.org/v1/gonum/graph/encoding/dot" |
15 | + ) |
16 | + |
17 | + type Encoding string |
18 | + |
19 | + func (e Encoding) String() string { |
20 | + return string(e) |
21 | + } |
22 | + |
23 | + const ( |
24 | + Json = Encoding("Json") |
25 | + Dot = Encoding("Dot") |
26 | + ) |
27 | + |
28 | + type Encoder interface { |
29 | + Encode(io.Writer) error |
30 | + } |
31 | + |
32 | + type dotEncoder struct { |
33 | + graph graph.Graph |
34 | + } |
35 | + |
36 | + func (d dotEncoder) Encode(w io.Writer) error { |
37 | + raw, err := dot.Marshal(d.graph, "", "", "", false) |
38 | + if err != nil { |
39 | + return err |
40 | + } |
41 | + _, err = bytes.NewBuffer(raw).WriteTo(w) |
42 | + return err |
43 | + } |
44 | + |
45 | + func NewEncoder(enc Encoding, g graph.Graph) Encoder { |
46 | + return dotEncoder{graph: g} |
47 | + } |
48 | diff --git a/graph.go b/graph.go |
49 | new file mode 100644 |
50 | index 0000000..de31843 |
51 | --- /dev/null |
52 | +++ b/graph.go |
53 | @@ -0,0 +1,49 @@ |
54 | + package main |
55 | + |
56 | + import ( |
57 | + "gonum.org/v1/gonum/floats" |
58 | + "gonum.org/v1/gonum/graph" |
59 | + "gonum.org/v1/gonum/graph/path" |
60 | + "gonum.org/v1/gonum/graph/simple" |
61 | + ) |
62 | + |
63 | + func SumWeights(route Route, weighters ...Weighter) float64 { |
64 | + var values []float64 |
65 | + for _, weighter := range weighters { |
66 | + values = append(values, weighter(route)) |
67 | + } |
68 | + return floats.Sum(values) |
69 | + } |
70 | + |
71 | + // Weighter evaluates the weight of a route |
72 | + // based on some criteria |
73 | + type Weighter func(Route) float64 |
74 | + |
75 | + // ByDistance sets the weight of a route based on the |
76 | + // distance between two airports. |
77 | + func ByDistance() Weighter { |
78 | + return func(r Route) float64 { |
79 | + return GetDistance(r.from, r.to) |
80 | + } |
81 | + } |
82 | + |
83 | + func Load(airports AirportMap, routes []Route, weighters ...Weighter) graph.Graph { |
84 | + g := simple.NewWeightedDirectedGraph(0.0, 0.0) |
85 | + for _, airport := range airports { |
86 | + g.AddNode(airport) |
87 | + } |
88 | + for _, route := range routes { |
89 | + g.SetWeightedEdge(SetWeight(route, SumWeights(route, weighters...))) |
90 | + } |
91 | + return g |
92 | + } |
93 | + |
94 | + func Find(start, end Airport, g graph.Graph) Itinerary { |
95 | + shortest := path.DijkstraFrom(start, g) |
96 | + path, weight := shortest.To(end) |
97 | + var airports []Airport |
98 | + for _, p := range path { |
99 | + airports = append(airports, p.(Airport)) |
100 | + } |
101 | + return Itinerary{stops: airports, weight: weight} |
102 | + } |
103 | diff --git a/main.go b/main.go |
104 | index dedd038..97e9c1b 100644 |
105 | --- a/main.go |
106 | +++ b/main.go |
107 | @@ -1,7 +1,6 @@ |
108 | package main |
109 | |
110 | import ( |
111 | - "encoding/json" |
112 | "fmt" |
113 | "os" |
114 | |
115 | @@ -9,53 +8,64 @@ import ( |
116 | ) |
117 | |
118 | func main() { |
119 | - app := cli.App("flights", "✈️:✈️ ✈️ ✈️ ✈️ ") |
120 | + app := cli.App("flights", "✈️ ✈️ ✈️ ✈️ ✈️ ") |
121 | + |
122 | app.Command("airports", "list all possible airports", func(cmd *cli.Cmd) { |
123 | - var ( |
124 | - asJson = cmd.BoolOpt("json", false, "use json encoding") |
125 | - ) |
126 | cmd.Action = func() { |
127 | - airports := Ordered(Airports()) |
128 | - if *asJson { |
129 | - maybe(json.NewEncoder(os.Stdout).Encode(airports)) |
130 | - } else { |
131 | - for _, airport := range airports { |
132 | - fmt.Println(airport) |
133 | - } |
134 | + airports := Airports() |
135 | + for _, airport := range airports.Ordered() { |
136 | + fmt.Println(airport) |
137 | + } |
138 | + } |
139 | + }) |
140 | + |
141 | + app.Command("routes", "list all possible routes", func(cmd *cli.Cmd) { |
142 | + cmd.Action = func() { |
143 | + for _, route := range Routes(Airports()) { |
144 | + fmt.Println(route) |
145 | } |
146 | } |
147 | }) |
148 | |
149 | - app.Command("route", "find all possible routes", func(cmd *cli.Cmd) { |
150 | - cmd.Spec = "[OPTIONS] ARRIVAL DESTINATION" |
151 | + app.Command("route", "find the best possible routes", func(cmd *cli.Cmd) { |
152 | + cmd.Spec = "[OPTIONS] DEPARTURE ARRIVAL" |
153 | var ( |
154 | - asJson = cmd.BoolOpt("json", false, "use json encoding") |
155 | - threshold = cmd.IntOpt("threshold", 100, "airport distance threshold") |
156 | - arrivalCode = cmd.StringArg("ARRIVAL", "", "starting airport") |
157 | - departureCode = cmd.StringArg("DESTINATION", "", "ending airport") |
158 | + //threshold = cmd.IntOpt("threshold", 100, "airport distance threshold") |
159 | + departureCode = cmd.StringArg("DEPARTURE", "", "starting airport") |
160 | + arrivalCode = cmd.StringArg("ARRIVAL", "", "ending airport") |
161 | ) |
162 | cmd.Action = func() { |
163 | airports := Airports() |
164 | - if _, ok := airports[*arrivalCode]; !ok { |
165 | + if !airports.HasCode(*arrivalCode) { |
166 | maybe(fmt.Errorf("bad arrival code: %s", *arrivalCode)) |
167 | } |
168 | - if _, ok := airports[*departureCode]; !ok { |
169 | + if !airports.HasCode(*departureCode) { |
170 | maybe(fmt.Errorf("bad departure code: %s", *departureCode)) |
171 | } |
172 | - departures, arrivals := FindRoutes(float64(*threshold), airports, airports[*arrivalCode], airports[*departureCode]) |
173 | - if *asJson { |
174 | - data := [][]Airport{departures, arrivals} |
175 | - maybe(json.NewEncoder(os.Stdout).Encode(data)) |
176 | - } else { |
177 | - fmt.Println("Departures: ") |
178 | - for _, departure := range departures { |
179 | - fmt.Println(departure) |
180 | - } |
181 | - fmt.Println("Arrivals: ") |
182 | - for _, arrival := range arrivals { |
183 | - fmt.Println(arrival) |
184 | + //shortest, weight := Load(airports.Airport(*departureCode), airports.Airport(*arrivalCode), opts) |
185 | + fmt.Println(Find( |
186 | + airports.Airport(*departureCode), |
187 | + airports.Airport(*arrivalCode), |
188 | + Load(airports, Routes(airports), ByDistance()), |
189 | + )) |
190 | + //fmt.Println(shortest, weight) |
191 | + //maybe(NewEncoder(Json, Load(airports, Routes(airports))).Encode(os.Stdout)) |
192 | + /* |
193 | + departures, arrivals := FindRoutes(float64(*threshold), airports, airports[*arrivalCode], airports[*departureCode]) |
194 | + if *asJson { |
195 | + data := [][]Airport{departures, arrivals} |
196 | + maybe(json.NewEncoder(os.Stdout).Encode(data)) |
197 | + } else { |
198 | + fmt.Println("Departures: ") |
199 | + for _, departure := range departures { |
200 | + fmt.Println(departure) |
201 | + } |
202 | + fmt.Println("Arrivals: ") |
203 | + for _, arrival := range arrivals { |
204 | + fmt.Println(arrival) |
205 | + } |
206 | } |
207 | - } |
208 | + */ |
209 | } |
210 | }) |
211 | maybe(app.Run(os.Args)) |
212 | diff --git a/types.go b/types.go |
213 | index 2ccfa5c..c60b260 100644 |
214 | --- a/types.go |
215 | +++ b/types.go |
216 | @@ -1,9 +1,44 @@ |
217 | package main |
218 | |
219 | import ( |
220 | + "bytes" |
221 | "fmt" |
222 | + "hash/fnv" |
223 | + "sort" |
224 | + |
225 | + "gonum.org/v1/gonum/graph" |
226 | ) |
227 | |
228 | + // Map of route keys e.g. ORDJFK and |
229 | + // slice of weights to apply |
230 | + type WeightMap map[string][]float64 |
231 | + |
232 | + type AirportMap map[string]Airport |
233 | + |
234 | + func (a AirportMap) Airport(id string) Airport { |
235 | + return a[id] |
236 | + } |
237 | + |
238 | + func (a AirportMap) HasCode(code string) bool { |
239 | + _, ok := a[code] |
240 | + return ok |
241 | + } |
242 | + |
243 | + func (a AirportMap) Ordered() []Airport { |
244 | + var ( |
245 | + sorted []Airport |
246 | + codes []string |
247 | + ) |
248 | + for key, _ := range a { |
249 | + codes = append(codes, key) |
250 | + } |
251 | + sort.Strings(codes) |
252 | + for _, code := range codes { |
253 | + sorted = append(sorted, a[code]) |
254 | + } |
255 | + return sorted |
256 | + } |
257 | + |
258 | type Airport struct { |
259 | // ICAO code |
260 | Code string |
261 | @@ -15,5 +50,52 @@ type Airport struct { |
262 | } |
263 | |
264 | func (a Airport) String() string { |
265 | - return fmt.Sprintf("%s:%s:%s:%s:%f:%f", a.Code, a.Name, a.City, a.Country, a.Latitude, a.Longitude) |
266 | + return fmt.Sprintf( |
267 | + "%s:%s:%s", |
268 | + a.Code, a.City, a.Country, |
269 | + ) |
270 | + } |
271 | + |
272 | + func (a Airport) DOTID() string { return fmt.Sprintf("\"%s\"", a.Code) } |
273 | + |
274 | + func (a Airport) ID() int64 { |
275 | + h := fnv.New64a() |
276 | + h.Write([]byte(a.String())) |
277 | + return int64(h.Sum64()) |
278 | + } |
279 | + |
280 | + type Route struct { |
281 | + to Airport |
282 | + from Airport |
283 | + weight float64 |
284 | + } |
285 | + |
286 | + func (r Route) String() string { |
287 | + return fmt.Sprintf("%s->%s", r.from, r.to) |
288 | + } |
289 | + |
290 | + func (r Route) To() graph.Node { return r.to } |
291 | + func (r Route) From() graph.Node { return r.from } |
292 | + func (r Route) Weight() float64 { return r.weight } |
293 | + |
294 | + func SetWeight(r Route, weight float64) Route { |
295 | + return Route{to: r.to, from: r.from, weight: weight} |
296 | + } |
297 | + |
298 | + type Itinerary struct { |
299 | + // Ordered list of airports |
300 | + stops []Airport |
301 | + weight float64 |
302 | + } |
303 | + |
304 | + func (i Itinerary) String() string { |
305 | + buf := bytes.NewBuffer(nil) |
306 | + buf.WriteString(fmt.Sprintf("%f:", i.weight)) |
307 | + for j, stop := range i.stops { |
308 | + buf.WriteString(stop.String()) |
309 | + if j+1 != len(i.stops) { |
310 | + buf.WriteString("-->") |
311 | + } |
312 | + } |
313 | + return buf.String() |
314 | } |
315 | diff --git a/util.go b/util.go |
316 | index 59504c8..ba3c08b 100644 |
317 | --- a/util.go |
318 | +++ b/util.go |
319 | @@ -6,7 +6,6 @@ import ( |
320 | "io" |
321 | "math" |
322 | "os" |
323 | - "sort" |
324 | "strconv" |
325 | "strings" |
326 | ) |
327 | @@ -68,39 +67,56 @@ func parseAirport(row []string) *Airport { |
328 | return airport |
329 | } |
330 | |
331 | - func Airports() map[string]Airport { |
332 | + //2B,410,AER,2965,KZN,2990,,0,CR2 |
333 | + func parseRoute(airports AirportMap, row []string) *Route { |
334 | + if len(row) != 9 { |
335 | + return nil |
336 | + } |
337 | + start, end := row[2], row[4] |
338 | + if airports.HasCode(start) && airports.HasCode(end) { |
339 | + if start != end { |
340 | + return &Route{ |
341 | + to: airports.Airport(end), |
342 | + from: airports.Airport(start), |
343 | + } |
344 | + } else { |
345 | + return nil |
346 | + } |
347 | + } else { |
348 | + return nil |
349 | + } |
350 | + } |
351 | + |
352 | + func Airports() AirportMap { |
353 | airports := map[string]Airport{} |
354 | - reader := csv.NewReader(strings.NewReader(data)) |
355 | + reader := csv.NewReader(strings.NewReader(airportData)) |
356 | reader.Comma = ':' |
357 | for { |
358 | row, err := reader.Read() |
359 | - if err != nil { |
360 | - if err == io.EOF { |
361 | - return airports |
362 | - } |
363 | - panic(err) |
364 | + if err == io.EOF { |
365 | + return airports |
366 | } |
367 | airport := parseAirport(row) |
368 | if airport != nil { |
369 | airports[airport.Code] = *airport |
370 | } |
371 | } |
372 | - return airports |
373 | } |
374 | |
375 | - func Ordered(airports map[string]Airport) []Airport { |
376 | - var ( |
377 | - sorted []Airport |
378 | - codes []string |
379 | - ) |
380 | - for key, _ := range airports { |
381 | - codes = append(codes, key) |
382 | - } |
383 | - sort.Strings(codes) |
384 | - for _, code := range codes { |
385 | - sorted = append(sorted, airports[code]) |
386 | + func Routes(airports AirportMap) []Route { |
387 | + var routes []Route |
388 | + reader := csv.NewReader(strings.NewReader(routeData)) |
389 | + reader.Comma = ',' |
390 | + for { |
391 | + row, err := reader.Read() |
392 | + if err == io.EOF { |
393 | + return routes |
394 | + } |
395 | + route := parseRoute(airports, row) |
396 | + if route != nil { |
397 | + routes = append(routes, *route) |
398 | + } |
399 | } |
400 | - return sorted |
401 | } |
402 | |
403 | func maybe(err error) { |