-
Notifications
You must be signed in to change notification settings - Fork 4
/
org-tfl.el
1344 lines (1243 loc) · 58 KB
/
org-tfl.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
;;; org-tfl.el --- Transport for London meets Orgmode
;; Copyright (C) 2015 2016 by David Zuber
;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
;; Version: 0.4.0
;; Author: storax (David Zuber), <zuber [dot] david [at] gmx [dot] de>
;; URL: https://github.com/storax/org-tfl
;; Package-Requires: ((org "0.16.2") (cl-lib "0.5") (emacs "24.1"))
;; Keywords: org, tfl
;;; Commentary:
;; Use the Transport For London API in Emacs, powered by org-mode.
;; For ambiguous results, `completing-read' is now used instead of helm.
;; Commands:
;;
;; Below are complete command list:
;;
;; `org-tfl-jp'
;; Plan a journey and view the result in a buffer.
;; `org-tfl-jp-org'
;; Plan a journey and insert a subheading with a special link.
;; The content is the journey result. Open the link to update it.
;; Use the scheduling function of org mode to change the date.
;; All other options are set via properties.
;;
;; Customizable Options:
;;
;; Below are customizable option list:
;;
;; `org-tfl-api-id'
;; Your Application ID for the TfL API. You don't need one
;; for personal use. It's IP locked anyway.
;; `org-tfl-api-key'
;; Your Application KEY for the TfL API. You don't need one
;; for personal use. It's IP locked anyway.
;; `org-tfl-map-width'
;; The width in pixels of static maps.
;; `org-tfl-map-height'
;; The height in pixels of static maps.
;; `org-tfl-map-type'
;; The map type. E.g. "roadmap", "terrain", "satellite", "hybrid".
;; `org-tfl-map-path-color'
;; The color of the path of static maps.
;; `org-tfl-map-path-weight'
;; The storke weight of paths of static maps.
;; `org-tfl-map-start-marker-color'
;; The path color of static maps.
;; `org-tfl-map-start-marker-color'
;; The start marker color of static maps.
;; `org-tfl-map-end-marker-color'
;; The end marker color of static maps.
;; `org-tfl-time-format-string'
;; The format string to display time.
;; `org-tfl-date-format-string'
;; The format string to display dates.
;; Installation:
;; Add the following to your Emacs init file:
;;
;; (require 'org-tfl)
;;; Code:
(require 'url)
(require 'url-http)
(require 'json)
(require 'cl-lib)
(require 'org)
(require 'org-element)
(defgroup org-tfl nil
"Org mode Transport for London."
:group 'org)
(defcustom org-tfl-api-id nil
"The application id for the Transport for London API."
:type 'string
:group 'org-tfl)
(defcustom org-tfl-api-key nil
"The application key for the Transport for London API."
:type 'string
:group 'org-tfl)
(defcustom org-tfl-map-width 800
"The width of static maps."
:type 'integer
:group 'org-tfl)
(defcustom org-tfl-map-height 800
"The height of static maps."
:type 'integer
:group 'org-tfl)
(defcustom org-tfl-map-type "roadmap"
"The type of static maps."
:options '("roadmap" "terrain" "satellite" "hybrid")
:group 'org-tfl)
(defcustom org-tfl-map-path-color "0xff0000ff"
"The path color of static maps."
:type 'string
:group 'org-tfl)
(defcustom org-tfl-map-path-weight 5
"The path weight of static maps."
:type 'integer
:group 'org-tfl)
(defcustom org-tfl-map-start-marker-color "blue"
"The start marker color of static maps."
:type 'string
:group 'org-tfl)
(defcustom org-tfl-map-end-marker-color "red"
"The end marker color of static maps."
:type 'string
:group 'org-tfl)
(defvar url-http-end-of-headers nil
"The location in a buffer of a http response that's at the end of headers.")
(defvar org-tfl-api-base-url "https://api.tfl.gov.uk/"
"The base url to the TFL API.")
(defvar org-tfl-api-jp "Journey/JourneyResults/%s/to/%s"
"API endpoint for the journey planner.")
;; Journey Planner context
(defvar org-tfl-jp-arg-from nil)
(defvar org-tfl-jp-arg-to nil)
(defvar org-tfl-jp-arg-via nil)
(defvar org-tfl-jp-arg-nationalSearch nil)
(defvar org-tfl-jp-arg-date nil)
(defvar org-tfl-jp-arg-time nil)
(defvar org-tfl-jp-arg-timeIs "Departing")
(defvar org-tfl-jp-arg-journeyPreference "leasttime")
(defvar org-tfl-jp-arg-mode nil)
(defvar org-tfl-jp-arg-accessibilityPreference nil)
(defvar org-tfl-jp-arg-fromName nil)
(defvar org-tfl-jp-arg-toName nil)
(defvar org-tfl-jp-arg-viaName nil)
(defvar org-tfl-jp-arg-maxTransferMinutes nil)
(defvar org-tfl-jp-arg-maxWalkingMinutes nil)
(defvar org-tfl-jp-arg-walkingSpeed "average")
(defvar org-tfl-jp-arg-cyclePreference nil)
(defvar org-tfl-jp-arg-adjustment nil)
(defvar org-tfl-jp-arg-bikeProficiency nil)
(defvar org-tfl-jp-arg-alternativeCycle nil)
(defvar org-tfl-jp-arg-alternativeWalking t)
(defvar org-tfl-jp-arg-applyHtmlMarkup nil)
(defvar org-tfl-jp-arg-useMultiModalCall nil)
;; Disambiguations
(defvar org-tfl-jp-fromdis nil)
(defvar org-tfl-jp-todis nil)
(defvar org-tfl-jp-viadis nil)
(defvar org-tfl-org-buffer nil)
(defvar org-tfl-org-buffer-point nil)
(defvar org-tlf-from-history nil)
(defvar org-tlf-to-history nil)
(defvar org-tlf-via-history nil)
(cl-defun org-tfl-create-icon (path &optional (asc 80) (text " "))
"Return string with icon at PATH displayed with ascent ASC and TEXT."
(propertize text 'display
(create-image
(with-temp-buffer
(insert-file-contents path) (buffer-string))
nil t :ascent asc :mask 'heuristic)))
;; Icons
(defconst org-tfl-icon-cam
(org-tfl-create-icon (concat (file-name-directory load-file-name) "cam.svg")))
(defconst org-tfl-icon-location
(org-tfl-create-icon (concat (file-name-directory load-file-name) "location.svg")))
(defconst org-tfl-icon-tube
(org-tfl-create-icon (concat (file-name-directory load-file-name) "tube.svg")))
(defconst org-tfl-icon-overground
(org-tfl-create-icon (concat (file-name-directory load-file-name) "overground.svg")))
(defconst org-tfl-icon-bus
(org-tfl-create-icon (concat (file-name-directory load-file-name) "bus.svg")))
(defconst org-tfl-icon-train
(org-tfl-create-icon (concat (file-name-directory load-file-name) "train.svg")))
(defconst org-tfl-icon-walking
(org-tfl-create-icon (concat (file-name-directory load-file-name) "walking.svg")))
(defconst org-tfl-icon-dlr
(org-tfl-create-icon (concat (file-name-directory load-file-name) "dlr.svg")))
(defconst org-tfl-icon-coach
(org-tfl-create-icon (concat (file-name-directory load-file-name) "coach.svg")))
(defconst org-tfl-icon-river-bus
(org-tfl-create-icon (concat (file-name-directory load-file-name) "river-bus.svg")))
(defconst org-tfl-icon-replacement-bus
(org-tfl-create-icon (concat (file-name-directory load-file-name) "replacement-bus.svg")))
(defconst org-tfl-icon-disruption
(org-tfl-create-icon (concat (file-name-directory load-file-name) "disruption.svg")))
(defconst org-tfl-icon-information
(org-tfl-create-icon (concat (file-name-directory load-file-name) "information.svg")))
(defvar org-tfl-mode-icons
(list
(cons "coach" org-tfl-icon-coach)
(cons "overground" org-tfl-icon-overground)
(cons "river-bus" org-tfl-icon-river-bus)
(cons "dlr" org-tfl-icon-dlr)
(cons "bus" org-tfl-icon-bus)
(cons "replacement-bus" org-tfl-icon-replacement-bus)
(cons "tube" org-tfl-icon-tube)
(cons "walking" org-tfl-icon-walking)
(cons "national-rail" org-tfl-icon-train)
(cons "tflrail" org-tfl-icon-train)
(cons "international-rail" org-tfl-icon-train))
"Mapping of modes to icons.")
(defface org-tfl-bakerloo-face
'((t (:foreground "white" :background "#996633")))
"Bakerloo Line Face"
:group 'org-tfl)
(defface org-tfl-central-face
'((t (:foreground "white" :background "#CC3333")))
"Central Line Face"
:group 'org-tfl)
(defface org-tfl-circle-face
'((t (:foreground "white" :background "#FFCC00")))
"Circle Line Face"
:group 'org-tfl)
(defface org-tfl-district-face
'((t (:foreground "white" :background "#006633")))
"District Line Face"
:group 'org-tfl)
(defface org-tfl-hammersmith-face
'((t (:foreground "white" :background "#CC9999")))
"Hammersmith and City Line Face"
:group 'org-tfl)
(defface org-tfl-jubliee-face
'((t (:foreground "white" :background "#868F98")))
"Jubliee Line Face"
:group 'org-tfl)
(defface org-tfl-metropolitan-face
'((t (:foreground "white" :background "#660066")))
"Metropolitan Line Face"
:group 'org-tfl)
(defface org-tfl-northern-face
'((t (:foreground "white" :background "#000000")))
"Northern Line Face"
:group 'org-tfl)
(defface org-tfl-piccadilly-face
'((t (:foreground "white" :background "#000099")))
"Piccadilly Line Face"
:group 'org-tfl)
(defface org-tfl-victoria-face
'((t (:foreground "white" :background "#0099CC")))
"Victoria Line Face"
:group 'org-tfl)
(defface org-tfl-waterloo-face
'((t (:foreground "white" :background "#66CCCC")))
"Waterloo and City Line Face"
:group 'org-tfl)
(defvar org-tfl-line-faces
'(("Bakerloo line" 0 'org-tfl-bakerloo-face prepend)
("Central line" 0 'org-tfl-central-face prepend)
("Circle line" 0 'org-tfl-circle-face prepend)
("District line" 0 'org-tfl-district-face prepend)
("Hammersmith & City line" 0 'org-tfl-hammersmith-face prepend)
("Jubilee line" 0 'org-tfl-jubliee-face prepend)
("Metropolitan line" 0 'org-tfl-metropolitan-face prepend)
("Northern line" 0 'org-tfl-northern-face prepend)
("Piccadilly line" 0 'org-tfl-piccadilly-face prepend)
("Victoria line" 0 'org-tfl-victoria-face prepend)
("Waterloo and City line" 0 'org-tfl-waterloo-face prepend))
"Mapping of lines to faces.")
(defcustom org-tfl-time-format-string "%H:%M"
"String for 'format-time-string' to display time."
:type 'string
:group 'org-tfl)
(defcustom org-tfl-datetime-format-string "%H:%M %d.%m.%Y"
"String for 'format-time-string' to display date and time."
:type 'string
:group 'org-tfl)
(defun org-tfl-get (list &rest keys)
"Retrieve a value from a LIST with KEYS."
(let ((list list))
(while (and list (listp list) keys)
(setq list (cdr (assoc (car keys) list)))
(setq keys (cdr keys)))
(unless keys
list)))
(defun org-tfl-date-to-time (tfldate)
"Convert a TFLDATE string of the TFL API to time."
(string-match "\\([0-9]\\{4\\}\\)-\\([0-9]\\{2\\}\\)-\\([0-9]\\{2\\}\\)T\\([0-9]\\{2\\}\\):\\([0-9]\\{2\\}\\):\\([0-9]\\{2\\}\\)" tfldate)
(encode-time
(string-to-number (match-string 6 tfldate))
(string-to-number (match-string 5 tfldate))
(string-to-number (match-string 4 tfldate))
(string-to-number (match-string 3 tfldate))
(string-to-number (match-string 2 tfldate))
(string-to-number (match-string 1 tfldate))))
(defun org-tfl-format-date (tfldate)
"Format the TFLDATE string.
If it's the same day as today, 'org-tfl-time-format-string' is used.
If the date is another day, 'org-tfl-datetime-format-string' is used."
(let ((time (org-tfl-date-to-time tfldate)))
(if (zerop (- (time-to-days time)
(time-to-days (current-time))))
(format-time-string org-tfl-time-format-string time)
(format-time-string org-tfl-datetime-format-string time))))
(defun org-tfl-jp-format-mode-icons (legs)
"Return a formatted string with the mode icons for LEGS."
(mapconcat
(lambda (leg)
(or (org-tfl-get org-tfl-mode-icons (org-tfl-get leg 'mode 'id))
(org-tfl-get leg 'mode 'id)))
legs
" "))
(defun org-tfl-jp-format-leg-disruption-icon (leg)
"Return a disruption icon if there are disruptions for the given LEG."
(if (equal (org-tfl-get leg 'isDisrupted) :json-false)
""
(or
(cl-loop for disruption across (org-tfl-get leg 'disruptions)
unless (equal (org-tfl-get disruption 'category) "Information")
return (concat org-tfl-icon-disruption " "))
(concat org-tfl-icon-information " "))))
(defun org-tfl-chop (s len)
"If S is longer than LEN, wrap the words with newlines."
(with-temp-buffer
(insert s)
(let ((fill-column len))
(fill-region (point-min) (point-max)))
(buffer-substring-no-properties (point-min) (point-max))))
(defun org-tfl-jp-format-leg-disruptions (leg level)
"Return a formatted string with disruptions for the given LEG at 'org-mode' LEVEL."
(if (equal (cdr (assoc 'isDisrupted leg)) :json-false)
""
(format
"\n%s %sDisruptions\n%s"
(make-string level (string-to-char "*"))
(org-tfl-jp-format-leg-disruption-icon leg)
(mapconcat
`(lambda (disruption)
(format
"%s %s%s\n%s"
(make-string ,(+ level 1) (string-to-char "*"))
(if (equal "Information" (org-tfl-get disruption 'categoryDescription))
(concat org-tfl-icon-information " ")
(concat org-tfl-icon-disruption " "))
(org-tfl-get disruption 'categoryDescription)
(org-tfl-chop (org-tfl-get disruption 'description) ,fill-column)))
(org-tfl-get leg 'disruptions)
"\n"))))
(defun org-tfl-make-maps-url (path)
"Create a url for the given PATH to a google maps static map."
(let* ((pathclean (replace-regexp-in-string
"\\(\\]\\]\\|\\[\\[\\| \\)" ""
path))
(wplist (split-string pathclean "\\],\\[")))
(substring
(cl-loop for start from 0 to (length wplist) by 27 concat
(format
"[[org-tfl-map:http://maps.google.com/maps/api/staticmap?size=%sx%s&maptype=%s&path=color:%s|weight:%s|%s&markers=label:S|color:%s|%s&markers=label:E|color:%s|%s][Map%s]]\n"
org-tfl-map-width
org-tfl-map-height
org-tfl-map-type
org-tfl-map-path-color
org-tfl-map-path-weight
(mapconcat 'identity
(cl-subseq wplist (max 0 (- start 1)) (min (+ start 26) (length wplist)))
"|")
org-tfl-map-start-marker-color
(elt wplist (max 0 (- start 1)))
org-tfl-map-end-marker-color
(elt wplist (min (+ start 25) (- (length wplist) 1)))
(+ (/ start 27) 1)))
0 -1)))
(defun org-tfl-open-map-link (path)
"Show the map in the buffer."
(let* ((link (save-match-data (org-element-context)))
(start (org-element-property :begin link))
(end (org-element-property :end link))
(name (concat temporary-file-directory (make-temp-name "orgtflmap") ".png")))
(url-copy-file path name)
(delete-region start end)
(insert (format "[[file:%s]]" name))
(org-display-inline-images nil t start (+ end 9))))
(org-add-link-type "org-tfl-map" 'org-tfl-open-map-link)
(defun org-tfl-jp-format-steps (leg)
"Return a formatted string with a list of steps for the given LEG.
The string will be prefixed with a newline character."
(let ((steps (org-tfl-get leg 'instruction 'steps)))
(concat
"\n"
(org-tfl-make-maps-url (org-tfl-get leg 'path 'lineString))
(if (> (length steps) 0)
(concat
"\n"
(mapconcat
`(lambda (step)
(format
"- %s"
(org-tfl-get step 'description)))
steps
"\n"))
""))))
(defun org-tfl-jp-format-leg-detailed (leg level)
"Return a detailed formatted string for the given LEG at the given 'org-mode' LEVEL."
(format
"%s %s%s%s"
(make-string level (string-to-char "*"))
(org-tfl-get leg 'instruction 'detailed)
(org-tfl-jp-format-steps leg)
(org-tfl-jp-format-leg-disruptions leg level)))
(defun org-tfl-jp-format-leg (leg level)
"Return a formatted string for the given LEG at the given 'org-mode' LEVEL."
(format
"%s %3smin %s %s %s%s\n%s"
(make-string level (string-to-char "*"))
(org-tfl-get leg 'duration)
(org-tfl-format-date (org-tfl-get leg 'departureTime))
(or (org-tfl-get org-tfl-mode-icons (org-tfl-get leg 'mode 'id))
(org-tfl-get leg 'mode 'id))
(org-tfl-jp-format-leg-disruption-icon leg)
(org-tfl-get leg 'instruction 'summary)
(org-tfl-jp-format-leg-detailed leg (+ level 1))))
(defun org-tfl-jp-format-journey-disruption-icon (legs)
"Return a disruption icon if there are disruptions for the given LEGS."
(or (cl-loop for leg across legs
if (equal-including-properties
(org-tfl-jp-format-leg-disruption-icon leg)
(concat org-tfl-icon-disruption " "))
return (concat org-tfl-icon-disruption " "))
""))
(defun org-tfl-jp-format-journey (journey level)
"Return a formatted string for the given JOURNEY at the given 'org-mode' LEVEL."
(let ((legs (org-tfl-get journey 'legs)))
(format
"%s %4smin %s %s%s => %s\n%s"
(make-string level (string-to-char "*"))
(org-tfl-get journey 'duration)
(org-tfl-jp-format-mode-icons legs)
(org-tfl-jp-format-journey-disruption-icon legs)
(org-tfl-format-date (org-tfl-get journey 'startDateTime))
(org-tfl-format-date (org-tfl-get journey 'arrivalDateTime))
(mapconcat `(lambda (leg) (org-tfl-jp-format-leg leg ,(+ level 1)))
legs
"\n"))))
(defun org-tfl-jp-format-title (result)
"Return a formattes string suitable for a title of a Journey Planner RESULT."
(let ((date (org-tfl-format-date (org-tfl-get result 'searchCriteria 'dateTime)))
(journey (elt (org-tfl-get result 'journeys) 0)))
(if journey
(format
"%3smin %s %s%s => %s | %s to %s"
(org-tfl-get journey 'duration)
(org-tfl-jp-format-mode-icons (org-tfl-get journey 'legs))
(org-tfl-jp-format-journey-disruption-icon (org-tfl-get journey 'legs))
(org-tfl-format-date (org-tfl-get journey 'startDateTime))
(org-tfl-format-date (org-tfl-get journey 'arrivalDateTime))
(or org-tfl-jp-arg-fromName org-tfl-jp-arg-from)
(or org-tfl-jp-arg-toName org-tfl-jp-arg-to))
(format "%s to %s (%s): No journeys found!"
org-tfl-jp-arg-fromName org-tfl-jp-arg-toName date))))
(defun org-tfl-jp-format-itinerary-result (result level &optional heading)
"Return a nice formatted string of the given itinerary RESULT.
Heading in the given 'org mode' LEVEL.
No heading if HEADING is nil."
(concat
(if heading
(format
"%s %s:\n"
(make-string level (string-to-char "*"))
(org-tfl-jp-format-title result))
"")
(mapconcat
`(lambda (journey) (org-tfl-jp-format-journey journey ,(+ level 1)))
(org-tfl-get result 'journeys) "\n")))
(defun org-tfl-jp-itinerary-handler (result resulthandler)
"Let RESULT be handled by RESULTHANDLER."
(funcall resulthandler result))
(defun org-tfl-jp-itinerary-show-in-buffer (result)
"Show itinerary RESULT."
(let ((journeys (org-tfl-get result 'journeys))
(level (+ (or (org-current-level) 0) 1)))
(if (zerop (length journeys))
(message "No journeys found!")
(let ((buf (get-buffer-create "Itinerary Results")))
(display-buffer buf)
(with-current-buffer buf
(erase-buffer)
(org-mode)
(font-lock-add-keywords nil org-tfl-line-faces t)
(insert (org-tfl-jp-format-itinerary-result result level t))
(hide-sublevels (+ level 1)))))))
(defun org-tfl-jp-replace-link (pos desc)
"Replace the link description at POS with DESC."
(goto-char pos)
(let ((linkregion (org-in-regexp org-bracket-link-regexp 1))
(link (org-link-unescape (org-match-string-no-properties 1)))
(properties (delete '("FILE") (org-entry-properties pos 'all))))
(setq properties (delq (assoc "ITEM" properties) properties))
(add-to-list 'properties (cons "FROM" org-tfl-jp-arg-from))
(add-to-list 'properties (cons "TO" org-tfl-jp-arg-to))
(when org-tfl-jp-arg-via
(add-to-list 'properties (cons "VIA" org-tfl-jp-arg-via)))
(when org-tfl-jp-arg-fromName
(add-to-list 'properties (cons "FROMNAME" org-tfl-jp-arg-fromName)))
(when org-tfl-jp-arg-toName
(add-to-list 'properties (cons "TONAME" org-tfl-jp-arg-toName)))
(when org-tfl-jp-arg-viaName
(add-to-list 'properties (cons "VIANAME" org-tfl-jp-arg-viaName)))
(delete-region (car linkregion) (cdr linkregion))
(org-cut-subtree)
(org-insert-subheading nil)
(org-promote-subtree)
(setq org-tfl-org-buffer-point (point))
(insert (format "[[%s][%s]]\n\n" link desc))
(goto-char (car linkregion))
(dolist (prop (reverse properties))
(org-set-property (car prop) (cdr prop)))))
(defun org-tfl-jp-itinerary-insert-org (result)
"Insert itinerary RESULT in org mode."
(let ((journeys (org-tfl-get result 'journeys)))
(display-buffer org-tfl-org-buffer)
(with-current-buffer org-tfl-org-buffer
(font-lock-add-keywords nil org-tfl-line-faces t)
(org-tfl-jp-replace-link org-tfl-org-buffer-point (org-tfl-jp-format-title result))
(let* ((level (or (org-current-level) 0))
(element (org-element-at-point))
(beginning (org-element-property :contents-begin element)))
(when beginning
(goto-char beginning)
(setq element (org-element-at-point))
(while (and (or (equal (org-element-type element) 'property-drawer)
(equal (org-element-type element) 'drawer)
(equal (org-element-type element) 'planning))
(not (equal (org-element-property :end element) (point))))
(when (org-element-property :end element)
(goto-char (org-element-property :end element))
(setq element (org-element-at-point)))))
(unless beginning
(goto-char (org-element-property :end element)))
(if (or (equal (org-element-type element) 'property-drawer)
(equal (org-element-type element) 'drawer)
(equal (org-element-type element) 'planning))
(goto-char (- (point) 1))
(goto-char (- (point) 2)))
(insert (org-tfl-jp-format-itinerary-result result level nil))
(goto-char org-tfl-org-buffer-point)
(hide-subtree)
(org-cycle)))))
(defun org-tfl-jp-get-disambiguations (result)
"Set the disambiguation options from RESULT."
(setq org-tfl-jp-fromdis nil
org-tfl-jp-todis nil
org-tfl-jp-viadis nil
org-tfl-jp-fromdis
(or (org-tfl-get result 'fromLocationDisambiguation 'disambiguationOptions)
(equal (org-tfl-get result 'fromLocationDisambiguation 'matchStatus)
"identified"))
org-tfl-jp-todis
(or (org-tfl-get result 'toLocationDisambiguation 'disambiguationOptions)
(equal (org-tfl-get result 'toLocationDisambiguation 'matchStatus)
"identified"))
org-tfl-jp-viadis
(or (org-tfl-get result 'viaLocationDisambiguation 'disambiguationOptions)
(equal (org-tfl-get result 'viaLocationDisambiguation 'matchStatus)
"identified"))))
(defun org-tfl-jp-pp-disambiguation (candidate)
"Nice formatting for disambiguation CANDIDATE."
(let* ((place (assoc 'place candidate))
(type (eval (org-tfl-get place 'placeType)))
(modes (org-tfl-get place 'modes))
(commonName (org-tfl-get place 'commonName)))
(cond ((equal type "StopPoint")
(if modes
(concat " " (mapconcat
#'(lambda (mode) (or (org-tfl-get org-tfl-mode-icons mode) mode))
modes " ")
" "
commonName)
(concat " " commonName)))
((equal type "PointOfInterest")
(format " %s %s" org-tfl-icon-cam commonName))
((equal type "Address")
(format " %s %s" org-tfl-icon-location commonName))
('t
(format " %s: %s" type commonName)))))
(defun org-tfl-jp-transform-disambiguations (candidates)
"Transform disambiguation option CANDIDATES.
Result is a list of (DISPLAY . REAL) values."
(mapcar (lambda (cand) (cons (org-tfl-jp-pp-disambiguation cand) cand))
candidates))
(defun org-tfl-jp-resolve-completing-read (cands var commonvar name)
"Let the user select CANDS to set VAR and COMMONVAR.
NAME for the prompt section."
(let* ((candstf (org-tfl-jp-transform-disambiguations (eval cands)))
(option (cdr (assoc
(completing-read
name
candstf
nil t)
candstf))))
(setq cands nil)
(set commonvar (org-tfl-get option 'place 'commonName))
(set var (format "lonlat:\"%s,%s\""
(org-tfl-get option 'place 'lon)
(org-tfl-get option 'place 'lat)))))
(defun org-tfl-jp-resolve-disambiguation (resulthandler)
"Let the user choose from the disambiguation options.
If there are no options retrieve itinerary and call RESULTHANDLER."
(when (vectorp org-tfl-jp-fromdis)
(org-tfl-jp-resolve-completing-read
'org-tfl-jp-fromdis
'org-tfl-jp-arg-from
'org-tfl-jp-arg-fromName
(format "Select FROM location for %s: " org-tfl-jp-arg-from)))
(when (vectorp org-tfl-jp-todis)
(org-tfl-jp-resolve-completing-read
'org-tfl-jp-todis
'org-tfl-jp-arg-to
'org-tfl-jp-arg-toName
(format "Select TO location for %s: " org-tfl-jp-arg-to)))
(when (vectorp org-tfl-jp-viadis)
(org-tfl-jp-resolve-completing-read
'org-tfl-jp-viadis
'org-tfl-jp-arg-via
'org-tfl-jp-arg-viaName
(format "Select VIA location for %s: " org-tfl-jp-arg-via)))
(url-retrieve
(org-tfl-jp-make-url)
`(lambda (status &rest args)
(apply 'org-tfl-jp-handle ',resulthandler status args))))
(defun org-tfl-jp-disambiguation-handler (result resulthandler)
"Resolve disambiguation of RESULT and try again with RESULTHANDLER."
(org-tfl-jp-get-disambiguations result)
(if (and org-tfl-jp-fromdis org-tfl-jp-todis)
(org-tfl-jp-resolve-disambiguation resulthandler)
(if org-tfl-jp-fromdis
(message "Cannot resolve To Location: %s" org-tfl-jp-arg-to)
(message "Cannot resolve From Location: %s" org-tfl-jp-arg-from))))
(defvar org-tfl-jp-handlers
`(("Tfl.Api.Presentation.Entities.JourneyPlanner.ItineraryResult, Tfl.Api.Presentation.Entities"
. org-tfl-jp-itinerary-handler)
("Tfl.Api.Presentation.Entities.JourneyPlanner.DisambiguationResult, Tfl.Api.Presentation.Entities"
. org-tfl-jp-disambiguation-handler)))
(defun org-tfl-jp-make-url ()
"Create journey planner url.
For keys see 'org-tfl-jp-retrieve'."
(replace-regexp-in-string
"&+$" ""
(concat org-tfl-api-base-url
(format org-tfl-api-jp
(url-hexify-string org-tfl-jp-arg-from)
(url-hexify-string org-tfl-jp-arg-to))
"?"
(if (and org-tfl-api-jp org-tfl-api-key)
(format "app_id=%s&app_key=%s&" (or org-tfl-api-id "") (or org-tfl-api-key ""))
"")
(if org-tfl-jp-arg-via (format "via=%s&" (url-hexify-string org-tfl-jp-arg-via)) "")
(if org-tfl-jp-arg-nationalSearch
(format "nationalSearch=%s&" org-tfl-jp-arg-nationalSearch) "")
(if org-tfl-jp-arg-date (format "date=%s&" org-tfl-jp-arg-date) "")
(if org-tfl-jp-arg-time (format "time=%s&" org-tfl-jp-arg-time) "")
(format "timeIs=%s&" org-tfl-jp-arg-timeIs)
(format "journeyPreference=%s&" org-tfl-jp-arg-journeyPreference)
(if org-tfl-jp-arg-mode (format "mode=%s&" org-tfl-jp-arg-mode) "")
(if org-tfl-jp-arg-accessibilityPreference (format "accessibilityPreference=%s&"
org-tfl-jp-arg-accessibilityPreference) "")
(if org-tfl-jp-arg-fromName
(format "fromName=%s&" (url-hexify-string org-tfl-jp-arg-fromName)) "")
(if org-tfl-jp-arg-toName
(format "toName=%s&" (url-hexify-string org-tfl-jp-arg-toName)) "")
(if org-tfl-jp-arg-viaName
(format "viaName=%s&" (url-hexify-string org-tfl-jp-arg-viaName)) "")
(if org-tfl-jp-arg-maxTransferMinutes
(format "maxTransferMinutes=%s&" org-tfl-jp-arg-maxTransferMinutes) "")
(if org-tfl-jp-arg-maxWalkingMinutes
(format "maxWalkingMinutes=%s&" org-tfl-jp-arg-maxWalkingMinutes) "")
(format "average=%s&" org-tfl-jp-arg-walkingSpeed)
(if org-tfl-jp-arg-cyclePreference
(format "cyclePreference=%s&" org-tfl-jp-arg-cyclePreference) "")
(if org-tfl-jp-arg-adjustment (format "adjustment=%s&" org-tfl-jp-arg-adjustment) "")
(if org-tfl-jp-arg-bikeProficiency
(format "bikeProficiency=%s&" org-tfl-jp-arg-bikeProficiency) "")
(if org-tfl-jp-arg-alternativeCycle
(format "alternativeCycle=%s&" org-tfl-jp-arg-alternativeCycle) "")
(if org-tfl-jp-arg-alternativeWalking
(format "alternativeWalking=%s&" org-tfl-jp-arg-alternativeWalking) "")
(if org-tfl-jp-arg-applyHtmlMarkup
(format "applyHtmlMarkup=%s&" org-tfl-jp-arg-applyHtmlMarkup) "")
(if org-tfl-jp-arg-useMultiModalCall
(format "useMultiModalCall=%s&" org-tfl-jp-arg-useMultiModalCall) ""))))
(defun org-tfl-jp-handle-error (data response)
"Handle errors with DATA and RESPONSE."
(if (eq (nth 1 (car data)) 'http)
(message "HTTP %s: %s" (nth 2 (car data)) response)))
(defun org-tfl-jp-handle-redirect (data response)
"Handle redirect errors with DATA and RESPONSE."
(message "Got redirected. Are you sure you supplied the correct credentials?"))
(defun org-tfl-jp-handle (resulthandler status &rest args)
"Handle the result of a jp request with RESULTHANDLER.
If status is not successful other handlers are called STATUS.
ARGS are ignored."
(goto-char url-http-end-of-headers)
(cond ((eq (car status) :error)
(org-tfl-jp-handle-error (cdr status) (buffer-substring (point) (point-max))))
((eq (car status) :redirect)
(org-tfl-jp-handle-redirect (cdr status) (buffer-substring (point) (point-max))))
(t
(let* ((result (json-read))
(type (org-tfl-get result '$type))
(handler (org-tfl-get org-tfl-jp-handlers type)))
(funcall handler result resulthandler)))))
(cl-defun org-tfl-jp-retrieve
(from to &key
(via nil) (nationalSearch nil) (date nil) (time nil) (timeIs "Departing")
(journeyPreference "leasttime") (mode nil) (accessibilityPreference nil)
(fromName nil) (toName nil) (viaName nil) (maxTransferMinutes nil)
(maxWalkingMinutes nil) (walkingSpeed "average") (cyclePreference nil)
(adjustment nil) (bikeProficiency nil) (alternativeCycle nil)
(alternativeWalking nil) (applyHtmlMarkup nil) (useMultiModalCall nil)
(resulthandler 'org-tfl-jp-itinerary-show-in-buffer))
"Retrieve journey result FROM TO with PARAMS.
FROM and TO are locations and can be names, Stop-IDs or coordinates of the format
\"lonlat:0.12345,67.890\".
VIA can be an optional place between FROM and TO.
NATIONALSEARCH should be \"True\" for journeys outside London.
DATE of the journey in yyyyMMdd format.
TIME of the journey in HHmm format.
TIMEIS does the given DATE and TIME relate to departure or arrival, e.g.
\"Departing\" | \"Arriving\".
JOURNEYPREFERENCE \"leastinterchange\" | \"leasttime\" | \"leastwalking\".
MODE comma seperated list, possible options \"black-cab-as-customer,black-cab-as-driver,bus,cable-car,coach,cycle,cycle-hire,dlr,electric-car,goods-vehicle-as-driver,interchange-keep-sitting,interchange-secure,international-rail,motorbike-scooter,national-rail,overground,plane,private-car,private-coach-as-customer,private-coach-as-driver,private-hire-as-customer,private-hire-as-driver,replacement-bus,river-bus,river-tour,tflrail,tram,tube,walking\".
ACCESSIBILITYPREFERENCE comma seperated list, possible options \"noSolidStairs,noEscalators,noElevators,stepFreeToVehicle,stepFreeToPlatform\".
FROMNAME is the location name associated with a from coordinate.
TONAME is the location name associated with a to coordinate.
VIANAME is the location name associated with a via coordinate.
MAXTRANSFERMINUTES The max walking time in minutes for transfer eg. \"120\".
MAXWALKINGMINUTES The max walking time in minutes for journey eg. \"120\".
WALKINGSPEED \"slow\" | \"average\" | \"fast\".
CYCLEPREFERENCE \"allTheWay\" | \"leaveAtStation\" | \"takeOnTransport\" | \"cycleHire\".
ADJUSTMENT time adjustment command, e.g. \"TripFirst\" | \"TripLast\".
BIKEPROFICIENCY comma seperated list, possible options \"easy,moderate,fast\".
ALTERNATIVECYCLE Option to determine whether to return alternative cycling journey.
ALTERNATIVEWALKING Option to determine whether to return alternative walking journey.
APPLYHTMLMARKUP Flag to determine whether certain text (e.g. walking instructions) should be output with HTML tags or not.
USEMULTIMODALCALL A boolean to indicate whether or not to return 3 public transport journeys, a bus journey, a cycle hire journey, a personal cycle journey and a walking journey.
RESULTHANDLER is the function to call after retrieving the result."
(setq org-tfl-jp-arg-from from
org-tfl-jp-arg-to to
org-tfl-jp-arg-via via
org-tfl-jp-arg-fromName from
org-tfl-jp-arg-toName to
org-tfl-jp-arg-viaName via
org-tfl-jp-arg-nationalSearch nationalSearch
org-tfl-jp-arg-date date
org-tfl-jp-arg-time time
org-tfl-jp-arg-timeIs timeIs
org-tfl-jp-arg-journeyPreference journeyPreference
org-tfl-jp-arg-mode mode
org-tfl-jp-arg-accessibilityPreference accessibilityPreference
org-tfl-jp-arg-fromName fromName
org-tfl-jp-arg-toName toName
org-tfl-jp-arg-viaName viaName
org-tfl-jp-arg-maxTransferMinutes maxTransferMinutes
org-tfl-jp-arg-maxWalkingMinutes maxWalkingMinutes
org-tfl-jp-arg-walkingSpeed walkingSpeed
org-tfl-jp-arg-cyclePreference cyclePreference
org-tfl-jp-arg-adjustment adjustment
org-tfl-jp-arg-bikeProficiency bikeProficiency
org-tfl-jp-arg-alternativeCycle alternativeCycle
org-tfl-jp-arg-alternativeWalking alternativeWalking
org-tfl-jp-arg-applyHtmlMarkup applyHtmlMarkup
org-tfl-jp-arg-useMultiModalCall useMultiModalCall
org-tfl-jp-fromdis nil
org-tfl-jp-todis nil
org-tfl-jp-viadis nil)
(url-retrieve
(org-tfl-jp-make-url)
`(lambda (status &rest args)
(apply 'org-tfl-jp-handle ',resulthandler status args))))
;;;###autoload
(defun org-tfl-jp (from to via datetime timeIs)
"Plan journey FROM TO VIA at DATETIME.
TIMEIS if t, DATETIME is the departing time."
(interactive
(list (read-from-minibuffer "From: " nil nil nil 'org-tfl-from-history)
(read-from-minibuffer "To: " nil nil nil 'org-tfl-to-history)
(read-from-minibuffer "Via: " nil nil nil 'org-tfl-via-history)
(org-read-date t t)
(yes-or-no-p "Time is departure time? No for arrival time:")))
(let ((date (format-time-string "%Y%m%d" datetime))
(time (format-time-string "%H%M" datetime))
(timeis (if timeIs "Departing" "Arriving")))
(org-tfl-jp-retrieve from to
:via (if (equal "" via) nil via)
:date date :time time :timeIs timeis)))
(cl-defun org-tfl-jp-retrieve-org (from to &rest keywords &allow-other-keys)
"Use 'org-tfl-jp-itinerary-insert-org' as handlefunc.
Inserts the result in the current buffer.
For the rest see 'org-tfl-jp-retrieve'."
(setq org-tfl-org-buffer (current-buffer))
(setq org-tfl-org-buffer-point (point))
(apply 'org-tfl-jp-retrieve from to :resulthandler 'org-tfl-jp-itinerary-insert-org keywords))
(defun org-tfl-jp-open-org-link (&optional path)
"Open a org-tfl link. PATH is ignored. Properties of the paragraph are used instead."
(let* ((element (org-element-at-point))
(FROM (org-element-property :FROM element))
(TO (org-element-property :TO element))
(VIA (org-element-property :VIA element))
(NATIONALSEARCH (org-element-property :NATIONALSEARCH element))
(SCHEDULED (org-get-scheduled-time (point)))
(DATE nil)
(TIME nil)
(TIMEIS (or (org-element-property :TIMEIS element) "Departing"))
(JOURNEYPREFERENCE (or (org-element-property :JOURNEYPREFERENCE element) "leasttime"))
(MODE (org-element-property :MODE element))
(ACCESSIBILITYPREFERENCE (org-element-property :ACCESSIBILITYPREFERENCE element))
(FROMNAME (org-element-property :FROMNAME element))
(TONAME (org-element-property :TONAME element))
(VIANAME (org-element-property :VIANAME element))
(MAXTRANSFERMINUTES (org-element-property :MAXTRANSFERMINUTE element))
(MAXWALKINGMINUTES (org-element-property :MAXWALKINMINUTES element))
(WALKINGSPEED (or (org-element-property :WALKINGSPEED element) "average"))
(CYCLEPREFERENCE (org-element-property :CYCLEPREFERENCE element))
(ADJUSTMENT (org-element-property :ADJUSTMENT element))
(BIKEPROFICIENCY (org-element-property :BIKEPROFICIENCY element))
(ALTERNATIVECYCLE (org-element-property :ALTERNATIVECYCLE element))
(ALTERNATIVEWALKING (org-element-property :ALTERNATIVEWALKING element)))
(when SCHEDULED
(setq DATE (format-time-string "%Y%m%d" SCHEDULED))
(setq TIME (format-time-string "%H%M" SCHEDULED)))
(org-tfl-jp-retrieve-org
FROM TO :via VIA :nationalSearch NATIONALSEARCH :date DATE :time TIME
:timeIs TIMEIS :journeyPreference JOURNEYPREFERENCE :mode MODE
:accessibilityPreference ACCESSIBILITYPREFERENCE :fromName FROMNAME
:toName TONAME :viaName VIANAME :maxTransferMinutes MAXTRANSFERMINUTES
:maxWalkingMinutes MAXWALKINGMINUTES :walkingSpeed WALKINGSPEED
:cyclePreference CYCLEPREFERENCE :adjustment ADJUSTMENT :bikeProficiency BIKEPROFICIENCY
:alternativeCycle ALTERNATIVECYCLE :alternativeWalking ALTERNATIVEWALKING)))
(org-add-link-type "org-tfl" 'org-tfl-jp-open-org-link)
;;;###autoload
(defun org-tfl-jp-org (from to via datetime timeIs)
"Plan journey FROM TO VIA at DATETIME.
This creates a subheading and a link to update the result.
The leave of the subheading is the journey result.
TIMEIS if t, DATETIME is the departing time."
(interactive
(list (read-from-minibuffer "From: " nil nil nil 'org-tfl-from-history)
(read-from-minibuffer "To: " nil nil nil 'org-tfl-to-history)
(read-from-minibuffer "Via: " nil nil nil 'org-tfl-via-history)
(org-read-date t t)
(yes-or-no-p "Time is departure time? No for arrival time:")))
(let ((timeis (if timeIs "Departing" "Arriving")))
(org-insert-subheading nil)
(org-promote)
(insert "[[org-tfl:][Retrieving Information...]]")
(org-set-property "FROM" from)
(org-set-property "TO" to)
(unless (equal via "")
(org-set-property "VIA" via))
(org-schedule nil (format-time-string (cdr org-time-stamp-formats) datetime))
(if timeIs
(org-set-property "TIMEIS" "Departing")
(org-set-property "TIMEIS" "Arriving"))
(org-tfl-jp-open-org-link)))
;; FIX 300 status code for disambiguation result to set success to t.
;; If we do not do this, the TfL API will return a 300 status code and
;; url-retrieve will get stuck in "Spinning waiting for headers", which never
;; completes so the callback is never called.
;; Curiously it did work at first but after an update to
;; the most recent versions of all packages, it doesn't (or did from time to time).
(defun url-http-parse-headers ()
"Parse and handle HTTP specific headers.
Return t if and only if the current buffer is still active and
should be shown to the user."
;; The comments after each status code handled are taken from RFC
;; 2616 (HTTP/1.1)
(url-http-mark-connection-as-free (url-host url-current-object)
(url-port url-current-object)
url-http-process)
(if (or (not (boundp 'url-http-end-of-headers))
(not url-http-end-of-headers))
(error "Trying to parse headers in odd buffer: %s" (buffer-name)))
(goto-char (point-min))
(url-http-debug "url-http-parse-headers called in (%s)" (buffer-name))
(url-http-parse-response)
(mail-narrow-to-head)
;;(narrow-to-region (point-min) url-http-end-of-headers)
(let ((connection (mail-fetch-field "Connection")))
;; In HTTP 1.0, keep the connection only if there is a
;; "Connection: keep-alive" header.
;; In HTTP 1.1 (and greater), keep the connection unless there is a
;; "Connection: close" header
(cond
((string= url-http-response-version "1.0")
(unless (and connection
(string= (downcase connection) "keep-alive"))
(delete-process url-http-process)))
(t
(when (and connection
(string= (downcase connection) "close"))
(delete-process url-http-process)))))
(let* ((buffer (current-buffer))
(class (/ url-http-response-status 100))
(success nil)
;; other status symbols: jewelry and luxury cars
(status-symbol (cadr (assq url-http-response-status url-http-codes))))
(url-http-debug "Parsed HTTP headers: class=%d status=%d"
class url-http-response-status)
(when (url-use-cookies url-http-target-url)
(url-http-handle-cookies))
(pcase class
;; Classes of response codes
;;
;; 5xx = Server Error
;; 4xx = Client Error