diff --git a/src/main/lemondronor/circlebot.cljs b/src/main/lemondronor/circlebot.cljs index a06c10c..5836c3b 100644 --- a/src/main/lemondronor/circlebot.cljs +++ b/src/main/lemondronor/circlebot.cljs @@ -60,7 +60,7 @@ (let [url (->> [url "lat" lat "lon" lon - "dist" radius-nm] + "dist" (.toFixed radius-nm 1)] (map str) (string/join "/"))] (p/let [http-result (util/http-get url {:headers {:api-auth api-key}})] @@ -69,15 +69,10 @@ result)))) -;; We keep position reports going back this far. - -(def max-history-age-ms (* 25 60 1000)) - - ;; Given a vector of position history, removes old entries. -(defn prune-history [history now] - (let [h (filterv #(< (- now (:time %)) max-history-age-ms) history)] +(defn prune-history [history now config] + (let [h (filterv #(< (- now (:time %)) (:max-history-age-ms config)) history)] h)) @@ -109,9 +104,9 @@ updated-db)) -(defn prune-histories [db now] +(defn prune-histories [db now config] (reduce-kv (fn [m k v] - (assoc m k (update v :history prune-history now))) + (assoc m k (update v :history prune-history now config))) {} db)) @@ -144,10 +139,10 @@ (first args)) -(defn update-history-db [db new-data now] +(defn update-history-db [db new-data now config] (-> db (update-history-db-add-new-data new-data now) - (prune-histories now) + (prune-histories now config) (prune-records now))) @@ -170,12 +165,6 @@ (/ (.getTime (js/Date.)) 1)) -;; This is how many degrees of turning we need to see over -;; max-history-age-ms ms to consider it a potential circling aircraft. - -(def curviness-threshold-degrees 1440) - - (defn ac-desc [ac] (str (:icao ac) " " (:lat ac) " " (:lon ac) " #" (:registration ac) " " (:alt ac) " " (:curviness ac) " " @@ -194,15 +183,15 @@ image-path)) -(defn circling? [ac] - (and (> (geo/flight-curviness (:history ac)) curviness-threshold-degrees) +(defn circling? [ac config] + (and (> (geo/flight-curviness (:history ac)) (:curviness-threshold-degrees config)) (> (:alt ac) 300))) ;; Returns a vector of two elements, ;; [updated-database potentially-circling-aircraft] -(defn detect-circles [db now] +(defn detect-circles [db now config] (log-verbose "Detecting circles") (loop [old-db (seq db) new-db {} @@ -213,7 +202,7 @@ ac (assoc ac :curviness curviness :normalized-curviness (geo/flight-normalized-curviness (:history ac))) - currently-circling? (circling? ac) + currently-circling? (circling? ac config) previously-circling? (:started-circling-time ac)] (cond (and currently-circling? @@ -371,11 +360,6 @@ recent-hist)) -;; If the centroid of the aircraft's positions is less than this close -;; to an airport, then it's probably just doinf flight training. -(def minimum-airport-distance-km 2.5) -;;(def minimum-airport-distance-miles 0) - (defn process-potential-circle [ac config now] (p/let [icao (:icao ac) recent-positions (recent-history (:history ac)) @@ -393,11 +377,11 @@ (log-info "%s: Closest airport is %s, distance: %s km" (:icao ac) (:label airport-properties) (:distance airport-properties)) (log-info "%s: No airports nearby" (:icao ac))) - (if (and airport-properties (<= (:distance airport-properties) minimum-airport-distance-km)) + (if (and airport-properties (<= (:distance airport-properties) (:minimum-airport-distance-km config))) (log-info "%s: Filtering out because it's %s km (minimum is %s) from %s" (:icao ac) (:distance airport-properties) - minimum-airport-distance-km + (:minimum-airport-distance-km config) (:label airport-properties) ()) (do @@ -458,55 +442,88 @@ (p/recur (rest acs)))))) -(def history-db-path "advisory-circular.db") -(def secrets-path "secrets.yaml") +(def default-config + { + ;; We keep position reports going back this far. + :max-history-age-ms (* 25 60 1000) + ;; This is how many degrees of turning we need to see over + ;; max-history-age-ms ms to consider it a potential circling + ;; aircraft. + :curviness-threshold-degrees 1440 + ;; If the centroid of the aircraft's positions is less than this + ;; close to an airport, then it's probably just doinf flight + ;; training. + :minimum-airport-distance-km 2.5 + :history-db-path "advisory-circular.db" + :twitter {:enabled? true}}) -(defn build-config [secrets commander] - (-> (merge-with merge - secrets - {:adsbx {:url (.-adsbxUrl commander)}} - {:twitter {:enabled? (.-tweeting commander)}} - {:pelias {:url (.-peliasUrl commander)}}) - (assoc :basestation-sqb (.-basestationSqb commander) - :lat (.-lat commander) - :lon (.-lon commander) - :radius-nm (.-radius commander)))) +(defn build-config-from-commander [commander] + (cond-> {} + (.-adsbxUrl commander) + (assoc-in [:adsbx :url] (.-adsbxUrl commander)) + ;; Note that we're distinguishing from the situation where + ;; --tweeting or --no-tweeting is supplied, in which case the + ;; tweeting field will be true or false, from the situation where + ;; it's not suppled, in which case it will be undefined/nil. + (not (nil? (.-tweeting commander))) + (assoc-in [:twitter :enabled?] (.-tweeting commander)) + (.-peliasUrl commander) + (assoc-in [:pelias :url] (.-peliasUrl commander)) + (.-basestationSqb commander) + (assoc :basestation-sqb (.-basestationSqb commander)) + (.-lat commander) + (assoc :lat (.-lat commander)) + (.-lon commander) + (assoc :lon (.-lon commander)) + (.-radius commander) + (assoc :radius (.-radius commander)))) + + +(defn build-config [config cli-config secrets] + (util/deep-merge default-config config cli-config secrets)) (defn main [& args] (-> commander - (.requiredOption "--lat " "Latitude of the circle of region of interest" parse-number) - (.requiredOption "--lon " "Longitude of the circle of the region of interest" parse-number) - (.requiredOption "--adsbx-url " "ADSBX API url") - (.requiredOption "--pelias-url " "Base pelias geocoder URL") - (.option "--radius " "Radius of the circle of interest, in nautical miles" 20 parse-number) + (.option "--lat " "Latitude of the circle of region of interest" parse-number) + (.option "--lon " "Longitude of the circle of the region of interest" parse-number) + (.option "--adsbx-url " "ADSBX API url") + (.option "--pelias-url " "Base pelias geocoder URL") + (.option "--radius " "Radius of the circle of interest, in km" parse-number) (.option "--basestation-sqb " "Path to a basestation.sqb database file") - (.option "--no-tweeting" "Do not tweet.") + (.option "--tweeting" "Enables tweeting") + (.option "--no-tweeting" "Do not tweet") + (.option "--config " "Path to the configuration yaml file" "config.yaml") + (.option "--secrets " "Path to the secrets yaml file" "secrets.yaml") (.parse (.-argv js/process))) (let [start-time (current-time)] - (p/then (p/all [(read-history-db history-db-path) - (util/read-config secrets-path)]) - (fn [[db secrets]] - (p/let [config (build-config secrets commander) - data (get-adsbexchange-live-data - {:url (get-in config [:adsbx :url]) - :api-key (get-in config [:adsbx :api-key]) - :lat (:lat config) - :lon (:lon config) - :radius-nm (:radius-nm config)}) - now (current-time) - [new-db potential-circles] (-> db - (update-history-db (:aircraft data) now) - (detect-circles now))] - (p/do - (when potential-circles - (doseq [ac potential-circles] - (log-warn "%s: New circle detected: %s" (:icao ac) (ac-desc ac))) - (process-potential-circles potential-circles config now)) - (write-history-db new-db history-db-path) + (p/let [base-config (util/read-config (.-config commander)) + cli-config (build-config-from-commander commander) + secrets (util/read-config (.-secrets commander)) + config (build-config base-config cli-config secrets) + _1 (pprint/pprint base-config) + _2 (pprint/pprint cli-config) + _3 (pprint/pprint config) + db (read-history-db (:history-db-path config)) + data (get-adsbexchange-live-data + {:url (get-in config [:adsbx :url]) + :api-key (get-in config [:adsbx :api-key]) + :lat (:lat config) + :lon (:lon config) + :radius-nm (* (:radius-km config) 0.539957)}) + now (current-time) + [new-db potential-circles] (-> db + (update-history-db (:aircraft data) now config) + (detect-circles now config))] + (p/do + (when potential-circles + (doseq [ac potential-circles] + (log-warn "%s: New circle detected: %s" (:icao ac) (ac-desc ac))) + (process-potential-circles potential-circles config now)) + (write-history-db new-db (:history-db-path config)) (let [end-time (current-time)] (log-info "Completed processing in %f seconds: tracking %s aircraft; %s potential circles" (/ (- end-time start-time) 1000) (count new-db) - (count potential-circles))))))))) + (count potential-circles))))))) diff --git a/src/main/lemondronor/circlebot/util.cljs b/src/main/lemondronor/circlebot/util.cljs index 2171769..6837860 100644 --- a/src/main/lemondronor/circlebot/util.cljs +++ b/src/main/lemondronor/circlebot/util.cljs @@ -17,13 +17,15 @@ (.readFile fs-promises path (clj->js options))) -;; Writes a file, returns a promise that resolves to no arguments on success. +;; Writes a file, returns a promise that resolves to no arguments on +;; success. (defn write-file [path data options] (.writeFile fs-promises path data (clj->js options))) -;; Reads a YAML config file. Returns a promise that resolves to the parsed YAML. +;; Reads a YAML config file. Returns a promise that resolves to the +;; parsed YAML. (defn read-config [path] (log-verbose "Reading config file %s" path) @@ -48,3 +50,19 @@ (p/let [result (.get request (clj->js (merge {:url (str url) :gzip true} options)))] result))))) + + +;; From https://github.com/puppetlabs/clj-kitchensink/blob/cfea4a16e4d2e15a2d391131a163b4eeb60d872e/src/puppetlabs/kitchensink/core.clj#L311-L332 + +(defn deep-merge + "Deeply merges maps so that nested maps are combined rather than replaced. + For example: + (deep-merge {:foo {:bar :baz}} {:foo {:fuzz :buzz}}) + ;;=> {:foo {:bar :baz, :fuzz :buzz}} + ;; contrast with clojure.core/merge + (merge {:foo {:bar :baz}} {:foo {:fuzz :buzz}}) + ;;=> {:foo {:fuzz :quzz}} ; note how last value for :foo wins" + [& vs] + (if (every? map? vs) + (apply merge-with deep-merge vs) + (last vs))) diff --git a/src/test/lemondronor/circlebot/util_test.cljs b/src/test/lemondronor/circlebot/util_test.cljs new file mode 100644 index 0000000..c399578 --- /dev/null +++ b/src/test/lemondronor/circlebot/util_test.cljs @@ -0,0 +1,10 @@ +(ns lemondronor.circlebot.util-test + (:require [cljs.test :refer (deftest is testing)] + [lemondronor.circlebot.util :as util])) + +(deftest deep-merge + (is (= (util/deep-merge + {:adsbx {:url "http://bar"}} + {:adsbx {:url "http://foo"}} + {:adsbx {:secret "123"}}) + {:adsbx {:url "http://foo", :secret "123"}})))