locations.lisp 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. (in-package :cl-user)
  2. (defpackage #:timeliner.locations
  3. (:use :cl #:timeliner.utils :cl-mongo)
  4. (:export
  5. #:import-location-events
  6. #:takedown-import
  7. #:find-location-at
  8. #:point->doc
  9. #:on-cron))
  10. (in-package #:timeliner.locations)
  11. (defun reverse-geocode (lat lon)
  12. (let* ((data (yason:parse
  13. (flexi-streams:octets-to-string
  14. (drakma:http-request
  15. (format nil
  16. "http://open.mapquestapi.com/nominatim/v1/reverse.php?format=json&lat=~A&lon=~A"
  17. lat lon)))))
  18. (address (gethash "address" data)))
  19. (values (gethash "display_name" data)
  20. address)))
  21. (defmethod cl-mongo::bson-encode((key string) (value local-time:timestamp) &key (array nil))
  22. (cl-mongo::bson-encode key (cl-mongo::make-bson-time (ts->ms value)) :array array))
  23. (defun make-place-doc (loc)
  24. (let* ((point (cdr loc))
  25. (lat (geo:latitude-deg point))
  26. (lon (geo:longitude-deg point)))
  27. (multiple-value-bind (text address) (reverse-geocode lat lon)
  28. (kv
  29. (kv :ts (car loc))
  30. (kv :type "place")
  31. (kv :title text)
  32. (kv :language (gethash "country_code" address))
  33. (kv :loc (kv (kv :type "Point") ; GeoJSON Point
  34. (kv :coordinates (list lon lat))))))))
  35. (defun save-places (places)
  36. (loop for place in places
  37. do (db.insert "events" (make-place-doc place))))
  38. (defun extract-places (locs)
  39. (loop
  40. for loc in locs
  41. for point = (cdr loc)
  42. with last-place and last-loc and moved and need-add
  43. when last-loc do (setf moved
  44. (geo:distance>=
  45. (geo:distance-between
  46. point
  47. (cdr last-loc))
  48. (geo:distance-meters 100)))
  49. when moved do (setf last-loc loc ; Store loc as possible place
  50. need-add t)
  51. when (and need-add last-place last-loc
  52. (geo:distance<
  53. (geo:distance-between
  54. (cdr last-place)
  55. (cdr last-loc))
  56. (geo:distance-meters 200))) do (setf need-add nil) ; Do not add same place after glitch
  57. when (and need-add
  58. (> (local-time:timestamp-difference
  59. (car loc)
  60. (car last-loc))
  61. 400)) ; When stayed in 100m for 6.6mins
  62. collect last-loc
  63. and do (setf need-add nil) (setf last-place last-loc)
  64. when (not last-loc) do (setf last-loc loc)))
  65. (defun doc->point (doc)
  66. (let ((coord (get-element "coordinates"
  67. (get-element "loc" doc))))
  68. (geo:point-deg (second coord) (first coord))))
  69. (defun doc->ts (doc)
  70. (local-time:unix-to-timestamp
  71. (floor (get-element "timestampMs" doc) 1000)))
  72. (defun docs->locs (docs)
  73. (loop for doc in docs
  74. collect (cons (doc->ts doc) (doc->point doc))))
  75. (defun ts->kv (field ts)
  76. (let ((nd (local-time:timestamp+ ts 1 :day)))
  77. ($between field
  78. (* 1000 (local-time:timestamp-to-unix ts))
  79. (* 1000 (local-time:timestamp-to-unix nd)))))
  80. (defun takedown-import (from-date)
  81. (let ((now (local-time:now)))
  82. (loop
  83. for ts = (local-time:parse-timestring from-date) then (local-time:timestamp+ ts 1 :day)
  84. for locs = (docs->locs (docs (db.sort "locations" (ts->kv "timestampMs" ts) :field "timestampMs" :limit 1500)))
  85. for places = (extract-places locs)
  86. when places do
  87. (format t "Inserting ~A places for ~A~%" (length places) ts)
  88. (save-places places)
  89. until (local-time:timestamp> ts now)
  90. finally (return ts))))
  91. (defvar *google-cookie-jar*)
  92. (defvar *google-manual-header*)
  93. (defun load-history (from &optional (to (local-time:timestamp+ from 1 :day)) (trim nil))
  94. (loop for loc in (cadadr (yason:parse
  95. (flexi-streams:octets-to-string
  96. (drakma:http-request
  97. "https://maps.google.com/locationhistory/b/0/apps/pvjson?t=0"
  98. :method :post
  99. :user-agent "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.114 Safari/537.36"
  100. :content (format nil "[null,~A,~A,~A]"
  101. (ts->ms from)
  102. (ts->ms to)
  103. (if trim "true" "false"))
  104. :cookie-jar *google-cookie-jar*
  105. :additional-headers (list
  106. (cons "x-manualheader" *google-manual-header*)
  107. '("referer" . "https://maps.google.com/locationhistory/b/0"))))))
  108. collect (cons (ms->ts (parse-integer (cadr loc)))
  109. (geo:point-deg (caddr loc) (cadddr loc)))))
  110. (defun get-history-xsrf-token ()
  111. (let* ((page (drakma:http-request
  112. "https://maps.google.com/locationhistory/b/0/"
  113. :user-agent "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.114 Safari/537.36"
  114. :cookie-jar *google-cookie-jar*))
  115. (needle "window.LatitudeServerConstants['XsrfToken'] = '")
  116. (start (search needle page)))
  117. (when start
  118. (subseq page
  119. (+ start (length needle))
  120. (position #\' page :start (+ start (length needle)))))))
  121. (defun point->doc (point)
  122. (when point
  123. (kv (kv :type "Point") ; GeoJSON Point
  124. (kv :coordinates (list (geo:longitude-deg point)
  125. (geo:latitude-deg point))))))
  126. (defun make-location-doc (loc)
  127. (kv
  128. (kv "timestampMs" (ts->ms (car loc)))
  129. (kv "loc" (point->doc (cdr loc)))))
  130. (defun save-locations (locations)
  131. (loop for loc in locations
  132. do (db.insert "locations" (make-location-doc loc))))
  133. (defun load-locations (from &optional (to (local-time:now)))
  134. (docs->locs (docs (db.sort "locations"
  135. ($between "timestampMs"
  136. (ts->ms from)
  137. (ts->ms to))
  138. :field "timestampMs"
  139. :limit (floor
  140. (* 11/600 ; 1 loc per 60 sec + 10%
  141. (local-time:timestamp-difference
  142. to from)))))))
  143. (defun import-location-events (from &optional (to (local-time:timestamp+ from 1 :day)))
  144. (let* ((*google-cookie-jar* (load-chrome-cookie-jar ".google.com"))
  145. (*google-manual-header* (get-history-xsrf-token))
  146. (locs (load-history from to nil)))
  147. (format t "Saving ~A locations for ~A~%" (length locs) from)
  148. (save-locations locs)
  149. (let* ((with-prev-locs (load-locations (local-time:timestamp- from 400 :sec) to))
  150. (places (extract-places with-prev-locs)))
  151. (format t "Saving ~A place events~%" (length places))
  152. (save-places places))))
  153. (defun find-location-at (ms)
  154. (let* ((ts (ms->ts ms))
  155. (docs (docs (db.sort "locations"
  156. ($between
  157. "timestampMs"
  158. (ts->ms (local-time:timestamp- ts 5 :minute))
  159. (ts->ms (local-time:timestamp+ ts 5 :minute)))
  160. :field "timestampMs" :limit 15)))
  161. less
  162. greater)
  163. (loop
  164. for doc in docs
  165. for docMs = (get-element "timestampMs" doc)
  166. when (<= docMs ms) do (setf less doc)
  167. when (and (not greater) (> docMs ms)) do (setf greater doc))
  168. (when (and less greater)
  169. (geo:interpolate-between-points
  170. (doc->point less)
  171. (doc->point greater)
  172. (/ (- ms (get-element "timestampMs" less))
  173. (- (get-element "timestampMs" greater)
  174. (get-element "timestampMs" less)))))))
  175. (defun on-cron ()
  176. (let* ((last-loc (first (docs (db.sort "locations" :all :field "timestampMs"
  177. :asc nil :limit 1))))
  178. (from (or (and last-loc (doc->ts last-loc))
  179. (local-time:timestamp- (today) 3 :day)))
  180. (to (local-time:timestamp-minimum
  181. (local-time:timestamp+ from 3 :day)
  182. (local-time:now))))
  183. (import-location-events from to)))