├── .github └── FUNDING.yml ├── .gitignore ├── .travis.yml ├── README.org ├── maxmind.sh ├── project.clj ├── src └── geocoder │ ├── bing.clj │ ├── geonames.clj │ ├── google.clj │ ├── maxmind.clj │ └── util.clj └── test └── geocoder ├── bing_test.clj ├── geonames_test.clj ├── google_test.clj ├── maxmind_test.clj └── utils.clj /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [r0man] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pom.xml 2 | *jar 3 | /lib/ 4 | /classes/ 5 | .lein-failures 6 | .lein-deps-sum 7 | /target 8 | /pom.xml.asc 9 | /checkouts/geo-clj 10 | /.lein-env 11 | /.nrepl-port 12 | /.eastwood 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | lein: lein 3 | before_script: ./maxmind.sh 4 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * geocoder-clj 2 | :PROPERTIES: 3 | :CUSTOM_ID: geocoder-clj 4 | :END: 5 | 6 | [[https://clojars.org/geocoder-clj][https://img.shields.io/clojars/v/geocoder-clj.svg]] 7 | [[https://travis-ci.org/r0man/geocoder-clj][https://travis-ci.org/r0man/geocoder-clj.svg]] 8 | [[https://versions.deps.co/r0man/geocoder-clj][https://versions.deps.co/r0man/geocoder-clj/status.svg]] 9 | [[https://versions.deps.co/r0man/geocoder-clj][https://versions.deps.co/r0man/geocoder-clj/downloads.svg]] 10 | 11 | A Clojure library for various geocoder services. 12 | 13 | ** Usage 14 | :PROPERTIES: 15 | :CUSTOM_ID: usage 16 | :END: 17 | 18 | *** Bing 19 | :PROPERTIES: 20 | :CUSTOM_ID: bing 21 | :END: 22 | 23 | #+BEGIN_SRC clojure :exports both :results verbatim 24 | (require '[geocoder.bing :as bing]) 25 | (def geocoder (bing/geocoder {:api-key "MY-KEY"})) 26 | (bing/geocode-address geocoder "Senefelderstraße 24, 10437 Berlin") 27 | (bing/geocode-location geocoder {:lat 52.54258 :lng 13.42299}) 28 | #+END_SRC 29 | 30 | *** Geonames 31 | :PROPERTIES: 32 | :CUSTOM_ID: geonames 33 | :END: 34 | 35 | #+BEGIN_SRC clojure :exports both :results verbatim 36 | (require '[geocoder.geonames :as geonames]) 37 | (def geocoder (geonames/geocoder {:api-key "MY-KEY"})) 38 | (geonames/geocode-address geocoder "Senefelderstraße 24, 10437 Berlin") 39 | (geonames/geocode-location geocoder {:lat 52.54258 :lng 13.42299}) 40 | #+END_SRC 41 | 42 | *** Google 43 | :PROPERTIES: 44 | :CUSTOM_ID: google 45 | :END: 46 | 47 | #+BEGIN_SRC clojure :exports both :results verbatim 48 | (require '[geocoder.google :as google]) 49 | (def geocoder (google/geocoder {:api-key "MY-KEY"})) 50 | (google/geocode-address geocoder "Senefelderstraße 24, 10437 Berlin") 51 | (google/geocode-location geocoder {:lat 52.54258 :lng 13.42299}) 52 | #+END_SRC 53 | 54 | *** Maxmind 55 | :PROPERTIES: 56 | :CUSTOM_ID: maxmind 57 | :END: 58 | 59 | #+BEGIN_SRC clojure :exports both :results verbatim 60 | (require '[geocoder.maxmind :as maxmind]) 61 | (def db (geonames/db "/usr/share/GeoIP/GeoIP.dat")) 62 | (maxmind/geocode-ip-address db "92.229.192.11") 63 | #+END_SRC 64 | 65 | ** License 66 | :PROPERTIES: 67 | :CUSTOM_ID: license 68 | :END: 69 | 70 | Copyright (C) 2013-2019 r0man 71 | 72 | Distributed under the Eclipse Public License, the same as Clojure. 73 | -------------------------------------------------------------------------------- /maxmind.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | mkdir -p ~/.maxmind 3 | cd ~/.maxmind 4 | wget -c http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.tar.gz -O GeoLite2-City.tar.gz 5 | tar xvfz GeoLite2-City.tar.gz 6 | mv GeoLite2-City_*/GeoLite2-City.mmdb . 7 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject geocoder-clj "0.3.8-SNAPSHOT" 2 | :description "A Clojure library for various geocoding services." 3 | :url "https://github.com/r0man/geocoder-clj" 4 | :min-lein-version "2.0.0" 5 | :deploy-repositories [["releases" :clojars]] 6 | :license {:name "Eclipse Public License" 7 | :url "http://www.eclipse.org/legal/epl-v10.html"} 8 | :dependencies [[cheshire "5.8.1"] 9 | [clj-http "3.9.1"] 10 | [com.maxmind.geoip2/geoip2 "2.12.0"] 11 | [inflections "0.13.2"] 12 | [org.clojure/clojure "1.10.0"]] 13 | :profiles {:dev {:dependencies [[clj-http "3.9.1"] 14 | [org.clojure/test.check "0.9.0"] 15 | [spec-provider "0.4.14"]]}} 16 | :plugins [[jonase/eastwood "0.3.5"]] 17 | :aliases {"ci" ["do" ["test"] ["lint"]] 18 | "lint" ["do" ["eastwood"]]}) 19 | -------------------------------------------------------------------------------- /src/geocoder/bing.clj: -------------------------------------------------------------------------------- 1 | (ns geocoder.bing 2 | (:require [geocoder.util :refer [fetch-json format-location]])) 3 | 4 | (defn city 5 | "Returns the city of `address`." 6 | [address] 7 | (:locality (:address address))) 8 | 9 | (defn country 10 | "Returns the country of `address`." 11 | [address] 12 | {:name (:country-region (:address address))}) 13 | 14 | (defn location 15 | "Returns the geographical location of `address`." 16 | [address] 17 | (let [[y x] (:coordinates (:point address))] 18 | {:lat y :lng x})) 19 | 20 | (defn street-name 21 | "Returns the street name of `address`." 22 | [address] 23 | (:address-line (:address address))) 24 | 25 | (defn postal-code 26 | "Returns the postal code of `address`." 27 | [address] 28 | (:postal-code (:address address))) 29 | 30 | (defn region 31 | "Returns the region of `address`." 32 | [address] 33 | (:state address)) 34 | 35 | (defn- request 36 | "Make a Bing geocode request map." 37 | [geocoder & [opts]] 38 | {:request-method :get 39 | :url "http://dev.virtualearth.net/REST/v1/Locations" 40 | :query-params 41 | (assoc opts :key (:api-key geocoder))}) 42 | 43 | (defn- fetch 44 | "Fetch and decode the Bing geocode response." 45 | [request] 46 | (->> (fetch-json request) 47 | :resource-sets 48 | (mapcat :resources))) 49 | 50 | (defn geocode-address 51 | "Geocode an address." 52 | [geocoder address & {:as opts}] 53 | (-> (request geocoder opts) 54 | (assoc-in [:query-params :query] address) 55 | (fetch))) 56 | 57 | (defn geocode-location 58 | "Geocode a geographical location." 59 | [geocoder location & {:as opts}] 60 | (-> (request geocoder opts) 61 | (update-in [:url] #(format "%s/%s" %1 (format-location location))) 62 | (fetch))) 63 | 64 | (defn geocoder 65 | "Returns a new Bing geocoder." 66 | [& [{:keys [api-key]}]] 67 | {:api-key api-key}) 68 | -------------------------------------------------------------------------------- /src/geocoder/geonames.clj: -------------------------------------------------------------------------------- 1 | (ns geocoder.geonames 2 | (:require [clojure.string :as str] 3 | [geocoder.util :as util])) 4 | 5 | (defn city 6 | [address] 7 | (:place-name address)) 8 | 9 | (defn country 10 | [address] 11 | {:iso-3166-1-alpha-2 (str/lower-case (:country-code address))}) 12 | 13 | (defn location [address] 14 | (select-keys address [:lat :lng])) 15 | 16 | (defn street-name [address] 17 | (:address-line (:address address))) 18 | 19 | (defn postal-code [address] 20 | (:postal-code address)) 21 | 22 | (defn region [address] 23 | (:admin-name1 address)) 24 | 25 | (defn- request 26 | "Make a geocode request map." 27 | [geocoder & [opts]] 28 | {:request-method :get 29 | :query-params 30 | (assoc opts :username (:api-key geocoder))}) 31 | 32 | (defn- fetch 33 | "Fetch and decode the Geonames geocode response." 34 | [request] 35 | (-> (util/fetch-json request) 36 | :postal-codes)) 37 | 38 | (defn geocode-address [geocoder address & [opts]] 39 | (-> (request geocoder opts) 40 | (assoc :url "http://api.geonames.org/postalCodeSearchJSON") 41 | (assoc-in [:query-params :placename] address) 42 | (fetch))) 43 | 44 | (defn geocode-location [geocoder location & [opts]] 45 | (-> (request geocoder opts) 46 | (assoc :url (str "http://api.geonames.org/findNearbyPostalCodesJSON")) 47 | (assoc-in [:query-params :lat] (:lat location)) 48 | (assoc-in [:query-params :lng] (:lng location)) 49 | (fetch))) 50 | 51 | (defn geocoder 52 | "Returns a new Geonames geocoder." 53 | [& [{:keys [api-key]}]] 54 | {:api-key api-key}) 55 | -------------------------------------------------------------------------------- /src/geocoder/google.clj: -------------------------------------------------------------------------------- 1 | (ns geocoder.google 2 | (:require [clojure.spec.alpha :as s] 3 | [clojure.string :as str] 4 | [geocoder.util :refer [fetch-json format-location]] 5 | [inflections.core :refer [underscore]])) 6 | 7 | ;; Results 8 | 9 | (s/def ::types (s/coll-of string?)) 10 | (s/def ::global-code string?) 11 | (s/def ::compound-code string?) 12 | (s/def ::plus-code (s/keys :req-un [::compound-code ::global-code])) 13 | (s/def ::place-id string?) 14 | (s/def ::partial-match boolean?) 15 | (s/def ::lng (s/and number? #(>= % -180.0) #(<= % 180.0))) 16 | (s/def ::lat (s/and number? #(>= % -90.0) #(<= % 90.0))) 17 | (s/def ::southwest (s/keys :req-un [::lat ::lng])) 18 | (s/def ::northeast (s/keys :req-un [::lat ::lng])) 19 | (s/def ::viewport (s/keys :req-un [::northeast ::southwest])) 20 | (s/def ::location-type string?) 21 | (s/def ::location (s/keys :req-un [::lat ::lng])) 22 | 23 | (s/def ::geometry 24 | (s/keys :req-un [::location ::location-type ::viewport])) 25 | 26 | (s/def ::formatted-address string?) 27 | (s/def ::short-name string?) 28 | (s/def ::long-name string?) 29 | 30 | (s/def ::address-components 31 | (s/coll-of (s/keys :req-un [::long-name ::short-name ::types]))) 32 | 33 | (s/def ::result 34 | (s/keys :req-un 35 | [::address-components 36 | ::formatted-address 37 | ::geometry] 38 | :opt-un 39 | [::place-id 40 | ::types 41 | ::partial-match 42 | ::plus-code])) 43 | 44 | (s/def ::results 45 | (s/nilable (s/coll-of ::result :gen-max 3))) 46 | 47 | ;; Geocoder 48 | 49 | (s/def ::sensor boolean?) 50 | (s/def ::language string?) 51 | (s/def ::key any?) 52 | (s/def ::query-params (s/keys :req-un [::key ::language ::sensor])) 53 | (s/def ::url string?) 54 | (s/def ::request-method keyword?) 55 | 56 | (s/def ::geocoder 57 | (s/keys :req-un [::query-params ::request-method ::url])) 58 | 59 | (defn- extract-type 60 | "Extract the address type from response." 61 | [response type] 62 | (let [type (underscore (name type))] 63 | (->> (:address-components response) 64 | (filter #(contains? (set (:types %)) type) ) 65 | (first)))) 66 | 67 | (defn long-name 68 | "Extract the long name of the address type from response." 69 | [response type] 70 | (:long-name (extract-type response type))) 71 | 72 | (defn short-name 73 | "Extract the short name of the address type from response." 74 | [response type] 75 | (:short-name (extract-type response type))) 76 | 77 | (defn city 78 | "Returns the city of `address`." 79 | [address] 80 | (:long-name (extract-type address :locality))) 81 | 82 | (defn country 83 | "Returns the country of `address`." 84 | [address] 85 | {:name (long-name address :country) 86 | :iso-3166-1-alpha-2 (some-> (short-name address :country) str/lower-case)}) 87 | 88 | (defn location 89 | "Returns the geographical location of `address`." 90 | [address] 91 | (:location (:geometry address))) 92 | 93 | (defn street-name 94 | "Returns the street name of `address`." 95 | [address] 96 | (long-name address :route)) 97 | 98 | (defn street-number 99 | "Returns the street number of `address`." 100 | [address] 101 | (long-name address :street-number)) 102 | 103 | (defn postal-code 104 | "Returns the postal code of `address`." 105 | [address] 106 | (long-name address :postal-code)) 107 | 108 | (defn region 109 | "Returns the region of `address`." 110 | [address] 111 | (long-name address :administrative-area-level-1)) 112 | 113 | (defn- request [request] 114 | (let [{:keys [error-message status] :as response} (fetch-json request)] 115 | (if (contains? #{"OK" "ZERO_RESULTS"} status) 116 | response 117 | (throw (ex-info (str "Geocode request failed: " error-message) response))))) 118 | 119 | (defn geocode-address 120 | "Geocode an address." 121 | [geocoder address & [opts]] 122 | (when-not (str/blank? address) 123 | (-> (update-in geocoder [:query-params] #(merge %1 opts)) 124 | (assoc-in [:query-params :address] address) 125 | (request) 126 | :results 127 | not-empty))) 128 | 129 | (s/fdef geocode-address 130 | :args (s/cat :geocoder ::geocoder 131 | :address string? 132 | :opts (s/? (s/nilable map?))) 133 | :ret ::results) 134 | 135 | (defn geocode-location 136 | "Geocode a geographical location." 137 | [geocoder location & [opts]] 138 | (-> (update-in geocoder [:query-params] #(merge %1 opts)) 139 | (assoc-in [:query-params :latlng] (format-location location)) 140 | (request) 141 | :results 142 | not-empty)) 143 | 144 | (s/fdef geocode-location 145 | :args (s/cat :geocoder ::geocoder 146 | :location ::location 147 | :opts (s/? (s/nilable map?))) 148 | :ret ::results) 149 | 150 | (defn geocoder 151 | "Returns a new Google Maps geocoder." 152 | [& [{:keys [api-key language sensor?]}]] 153 | {:request-method :get 154 | :url "https://maps.google.com/maps/api/geocode/json" 155 | :query-params 156 | {:key api-key 157 | :language (or language "en") 158 | :sensor (or sensor? false)}}) 159 | -------------------------------------------------------------------------------- /src/geocoder/maxmind.clj: -------------------------------------------------------------------------------- 1 | (ns geocoder.maxmind 2 | (:import [com.maxmind.geoip2 DatabaseReader$Builder] 3 | [com.maxmind.geoip2.exception AddressNotFoundException] 4 | [java.net InetAddress]) 5 | (:require [clojure.string :refer [lower-case]] 6 | [clojure.java.io :as io] 7 | [clojure.string :as str])) 8 | 9 | (def default-path 10 | (str (System/getenv "HOME") "/.maxmind/GeoLite2-City.mmdb")) 11 | 12 | (defn db 13 | "Make a Maxmind GeoIP database." 14 | [& [path]] 15 | (let [path (io/file (or path default-path))] 16 | (when (.exists path) 17 | (.build (DatabaseReader$Builder. path))))) 18 | 19 | (defn city 20 | "Returns the city of `address`." 21 | [result] 22 | (some-> (.getCity result) (.getName))) 23 | 24 | (defn country 25 | "Returns the country of `result`." 26 | [result] 27 | (let [country (.getCountry result)] 28 | {:name (.getName country) 29 | :iso-3166-1-alpha-2 (some-> (.getIsoCode country) str/lower-case)})) 30 | 31 | (defn location 32 | "Returns the location of `address`." 33 | [result] 34 | (when-let [location (.getLocation result)] 35 | {:lng (double (.getLongitude location)) 36 | :lat (double (.getLatitude location)) })) 37 | 38 | (defn region-id 39 | "Returns the region id of `address`." 40 | [address] 41 | (. address region)) 42 | 43 | (defn geocode-ip-address 44 | "Geocode an internet address using one of Maxmind's GeoIP 45 | databases." 46 | [db ip-address] 47 | (try 48 | (let [ip-address (InetAddress/getByName ip-address)] 49 | (let [result (.city db ip-address)] 50 | {:country (country result) 51 | :city (city result) 52 | :location (location result) 53 | ;; :region-id (region-id result) 54 | })) 55 | (catch AddressNotFoundException e 56 | nil))) 57 | 58 | (defn wrap-maxmind 59 | "Wrap the Maxmind middleware around a Ring handler." 60 | [handler db-path] 61 | (let [db (db db-path)] 62 | (fn [request] 63 | (if db 64 | (let [address (geocode-ip-address db (:remote-addr request))] 65 | (handler (assoc request :maxmind address))) 66 | (handler request))))) 67 | -------------------------------------------------------------------------------- /src/geocoder/util.clj: -------------------------------------------------------------------------------- 1 | (ns geocoder.util 2 | (:require [clj-http.client :as client] 3 | [clojure.string :refer [split]] 4 | [inflections.core :refer [hyphenate-keys]] 5 | [no.en.core :refer [parse-double]])) 6 | 7 | (defn parse-location [s] 8 | (let [parts (->> (split (str s) #"\s*,\s*") 9 | (map parse-double) 10 | (remove nil?))] 11 | (if (= 2 (count parts)) 12 | (zipmap [:lat :lng] parts )))) 13 | 14 | (defn format-location 15 | "Format `location` in latitude, longitude order." 16 | [location] 17 | (format "%s,%s" (:lat location) (:lng location))) 18 | 19 | (defn fetch-json 20 | "Send the request, parse the hyphenated JSON body of the response." 21 | [request] 22 | (try (->> (merge 23 | {:as :auto 24 | :accept "application/json" 25 | :throw-exceptions true 26 | :coerce :always} 27 | request) 28 | (client/request) 29 | :body 30 | hyphenate-keys) 31 | (catch Exception e 32 | (throw (ex-info (str "Geocode request failed: " (.getMessage e)) 33 | (hyphenate-keys (ex-data e))))))) 34 | -------------------------------------------------------------------------------- /test/geocoder/bing_test.clj: -------------------------------------------------------------------------------- 1 | (ns geocoder.bing-test 2 | (:require [clojure.test :refer :all] 3 | [geocoder.utils :refer [approx=]] 4 | [geocoder.bing :as bing :refer :all])) 5 | 6 | (def test-key "AhiRsght2jQhqZaHpUAvXhCjvNfyWxb0Xb4EwAW81LUgvBa68FcnL9mOFDLKoQGl") 7 | 8 | (def response 9 | {:name "Senefelderstraße 24, 10437 Berlin", 10 | :point {:type "Point", :coordinates [52.54254 13.423033]}, 11 | :address 12 | {:address-line "Senefelderstraße 24", 13 | :admin-district "BE", 14 | :country-region "Germany", 15 | :formatted-address "Senefelderstraße 24, 10437 Berlin", 16 | :locality "Berlin", 17 | :postal-code "10437"}, 18 | :confidence "High", 19 | :entity-type "Address"}) 20 | 21 | (deftest test-city 22 | (is (= "Berlin" (city response)))) 23 | 24 | (deftest test-country 25 | (let [country (country response)] 26 | (is (nil? (:iso-3166-1-alpha-2 country))) 27 | (is (= "Germany" (:name country))))) 28 | 29 | (deftest test-location 30 | (let [location (location response)] 31 | (is (approx= 52.54254 (:lat location))) 32 | (is (approx= 13.423033 (:lng location))))) 33 | 34 | (deftest test-street-name 35 | (is (= "Senefelderstraße 24" (street-name response)))) 36 | 37 | (deftest test-postal-code 38 | (is (= "10437" (postal-code response)))) 39 | 40 | (deftest test-region 41 | (is (nil? (region response)))) 42 | 43 | (deftest test-geocode-address 44 | (let [geocoder (bing/geocoder {:api-key test-key})] 45 | (is (empty? (geocode-address geocoder "xxxxxxxxxxxxxxxxxxxxx"))) 46 | (let [address (first (geocode-address geocoder "Senefelderstraße 24, Berlin"))] 47 | (is (= "Senefelderstraße 24" (street-name address))) 48 | (is (= "10437" (postal-code address))) 49 | (is (= "Berlin" (city address))) 50 | (is (nil? (region address))) 51 | (let [country (country address)] 52 | (is (nil? (:iso-3166-1-alpha-2 country))) 53 | (is (= "Germany" (:name country)))) 54 | (let [location (location address)] 55 | (is (approx= 13.42306 (:lng location))) 56 | (is (approx= 52.54255 (:lat location))))))) 57 | 58 | (deftest test-geocode-location 59 | (let [geocoder (bing/geocoder {:api-key test-key})] 60 | (is (empty? (geocode-location geocoder {:lat 0 :lng 0}))) 61 | (let [address (first (geocode-location geocoder {:lng 13.423033 :lat 52.54254}))] 62 | (is (= "SenefelderStraße 24" (street-name address))) 63 | (is (= "10437" (postal-code address))) 64 | (is (= "Berlin" (city address))) 65 | (is (nil? (region address))) 66 | (let [country (country address)] 67 | (is (nil? (:iso-3166-1-alpha-2 country))) 68 | (is (= "Germany" (:name country)))) 69 | (let [location (location address)] 70 | (is (approx= 13.423033 (:lng location))) 71 | (is (approx= 52.54254 (:lat location))))))) 72 | -------------------------------------------------------------------------------- /test/geocoder/geonames_test.clj: -------------------------------------------------------------------------------- 1 | (ns geocoder.geonames-test 2 | (:require [clojure.test :refer :all] 3 | [geocoder.utils :refer [approx=]] 4 | [geocoder.geonames :as geocoder])) 5 | 6 | (def test-key "geonamesclj") 7 | 8 | (def response 9 | {:admin-code1 "BE" 10 | :admin-name1 "Berlin" 11 | :admin-name3 "Kreisfreie Stadt Berlin" 12 | :country-code "DE" 13 | :lat 52.5161166666667 14 | :lng 13.38735 15 | :place-name "Berlin" 16 | :postal-code "10117"}) 17 | 18 | (deftest test-city 19 | (is (= "Berlin" (geocoder/city response)))) 20 | 21 | (deftest test-country 22 | (let [country (geocoder/country response)] 23 | (is (= "de" (:iso-3166-1-alpha-2 country))))) 24 | 25 | (deftest test-location 26 | (let [location (geocoder/location response)] 27 | (is (= 52.5161166666667 (:lat location))) 28 | (is (= 13.38735 (:lng location))))) 29 | 30 | (deftest test-street-name 31 | (is (nil? (geocoder/street-name response)))) 32 | 33 | (deftest test-postal-code 34 | (is (= "10117" (geocoder/postal-code response)))) 35 | 36 | (deftest test-region 37 | (is (= "Berlin" (geocoder/region response)))) 38 | 39 | (deftest test-geocode-address 40 | (let [geocoder (geocoder/geocoder {:api-key test-key})] 41 | (is (empty? (geocoder/geocode-address geocoder "xxxxxxxxxxxxxxxxxxxxx"))) 42 | (let [address (first (geocoder/geocode-address geocoder "Berlin"))] 43 | (is (nil? (geocoder/street-name address))) 44 | (is (= "10587" (geocoder/postal-code address))) 45 | (is (= "Berlin" (geocoder/city address))) 46 | (is (= "Berlin" (geocoder/region address))) 47 | (let [country (geocoder/country address)] 48 | (is (= "de" (:iso-3166-1-alpha-2 country))) 49 | (is (nil? (:name country)))) 50 | (let [location (geocoder/location address)] 51 | (is (approx= 52.5161166666667 (:lat location))) 52 | (is (approx= 13.319519141740923 (:lng location))))))) 53 | 54 | (deftest test-geocode-location 55 | (let [geocoder (geocoder/geocoder {:api-key test-key})] 56 | (is (empty? (geocoder/geocode-location geocoder {:lat 0 :lng 0}))) 57 | (let [address (first (geocoder/geocode-location geocoder {:lng 13.38735 :lat 52.5161166666667}))] 58 | (is (nil? (geocoder/street-name address))) 59 | (is (= "10117" (geocoder/postal-code address))) 60 | (is (= "Berlin" (geocoder/city address))) 61 | (is (= "Berlin" (geocoder/region address))) 62 | (let [country (geocoder/country address)] 63 | (is (= "de" (:iso-3166-1-alpha-2 country))) 64 | (is (nil? (:name country)))) 65 | (let [location (geocoder/location address)] 66 | (is (approx= 52.5161166666667 (:lat location))) 67 | (is (approx= 13.38735 (:lng location))))))) 68 | -------------------------------------------------------------------------------- /test/geocoder/google_test.clj: -------------------------------------------------------------------------------- 1 | (ns geocoder.google-test 2 | (:require [clojure.test :refer :all] 3 | [clojure.spec.test.alpha :as stest] 4 | [geocoder.google :as google :refer :all] 5 | [geocoder.util :as util] 6 | [geocoder.utils :refer [approx=]])) 7 | 8 | (stest/instrument) 9 | 10 | (def api-key 11 | "MY-API-KEY") 12 | 13 | (def address 14 | {:types ["street_address"], 15 | :geometry 16 | {:location {:lat 52.54258, :lng 13.42299}, 17 | :location-type "ROOFTOP", 18 | :viewport 19 | {:northeast {:lat 52.5439289802915, :lng 13.4243389802915}, 20 | :southwest {:lat 52.5412310197085, :lng 13.4216410197085}}}, 21 | :formatted-address "Senefelderstraße 24, 10437 Berlin, Germany", 22 | :address-components 23 | [{:long-name "24", :short-name "24", :types ["street_number"]} 24 | {:long-name "Senefelderstraße", 25 | :short-name "Senefelderstraße", 26 | :types ["route"]} 27 | {:long-name "Berlin", 28 | :short-name "Berlin", 29 | :types ["sublocality" "political"]} 30 | {:long-name "Berlin", 31 | :short-name "Berlin", 32 | :types ["locality" "political"]} 33 | {:long-name "Berlin", 34 | :short-name "Berlin", 35 | :types ["administrative_area_level_2" "political"]} 36 | {:long-name "Berlin", 37 | :short-name "Berlin", 38 | :types ["administrative_area_level_1" "political"]} 39 | {:long-name "Germany", 40 | :short-name "DE", 41 | :types ["country" "political"]} 42 | {:long-name "10437", :short-name "10437", :types ["postal_code"]}]}) 43 | 44 | (def geocode-address-response-ok 45 | {:results 46 | [{:address-components 47 | [{:long-name "24", :short-name "24", :types ["street_number"]} 48 | {:long-name "Senefelderstraße", 49 | :short-name "Senefelderstraße", 50 | :types ["route"]} 51 | {:long-name "Bezirk Pankow", 52 | :short-name "Bezirk Pankow", 53 | :types ["political" "sublocality" "sublocality_level_1"]} 54 | {:long-name "Berlin", 55 | :short-name "Berlin", 56 | :types ["locality" "political"]} 57 | {:long-name "Berlin", 58 | :short-name "Berlin", 59 | :types ["administrative_area_level_1" "political"]} 60 | {:long-name "Germany", 61 | :short-name "DE", 62 | :types ["country" "political"]} 63 | {:long-name "10437", :short-name "10437", :types ["postal_code"]}], 64 | :formatted-address "Senefelderstraße 24, 10437 Berlin, Germany", 65 | :geometry 66 | {:location {:lat 52.54258, :lng 13.42299}, 67 | :location-type "ROOFTOP", 68 | :viewport 69 | {:northeast {:lat 52.5439289802915, :lng 13.4243389802915}, 70 | :southwest {:lat 52.5412310197085, :lng 13.4216410197085}}}, 71 | :partial-match true, 72 | :place-id "ChIJf9QNQf5NqEcRVo31O1xb20c", 73 | :plus-code 74 | {:compound-code "GCVF+25 Berlin, Germany", 75 | :global-code "9F4MGCVF+25"}, 76 | :types ["street_address"]}], 77 | :status "OK"}) 78 | 79 | (def geocoder-result-zero 80 | {:results [] :status "ZERO_RESULTS"}) 81 | 82 | (deftest test-city 83 | (is (= "Berlin" (city address)))) 84 | 85 | (deftest test-country 86 | (let [country (country address)] 87 | (is (= "de" (:iso-3166-1-alpha-2 country))) 88 | (is (= "Germany" (:name country))))) 89 | 90 | (deftest test-location 91 | (let [location (location address)] 92 | (is (approx= 52.54258 (:lat location))) 93 | (is (approx= 13.42299 (:lng location))))) 94 | 95 | (deftest test-street-name 96 | (is (= "Senefelderstraße" (street-name address)))) 97 | 98 | (deftest test-street-number 99 | (is (= "24" (street-number address)))) 100 | 101 | (deftest test-postal-code 102 | (is (= "10437" (postal-code address)))) 103 | 104 | (deftest test-region 105 | (is (= "Berlin" (region address)))) 106 | 107 | (deftest test-geocode-address 108 | (let [geocoder (google/geocoder {:api-key api-key})] 109 | (is (nil? (geocode-address geocoder ""))) 110 | (with-redefs [util/fetch-json (constantly geocoder-result-zero)] 111 | (is (nil? (geocode-address geocoder "xxxxxxxxxxxxxxxxxxxxx")))) 112 | (with-redefs [util/fetch-json (constantly geocode-address-response-ok)] 113 | (let [[address] (geocode-address geocoder "Senefelderstraße 24, 10437 Berlin")] 114 | (is (= "Senefelderstraße" (street-name address))) 115 | (is (= "24" (street-number address))) 116 | (is (= "10437" (postal-code address))) 117 | (is (= "Berlin" (city address))) 118 | (is (= "Berlin" (region address))) 119 | (let [country (country address)] 120 | (is (= "de" (:iso-3166-1-alpha-2 country))) 121 | (is (= "Germany" (:name country)))) 122 | (let [location (location address)] 123 | (is (= 52.54258 (:lat location))) 124 | (is (= 13.42299 (:lng location)))))))) 125 | 126 | (deftest test-geocode-location 127 | (let [geocoder (google/geocoder {:api-key api-key})] 128 | (with-redefs [util/fetch-json (constantly geocoder-result-zero)] 129 | (is (nil? (geocode-location geocoder {:lat 0 :lng 0})))) 130 | (with-redefs [util/fetch-json (constantly geocode-address-response-ok)] 131 | (let [[address] (geocode-location geocoder {:lat 52.54258 :lng 13.42299})] 132 | (is (= "Senefelderstraße" (street-name address))) 133 | (is (= "24" (street-number address))) 134 | (is (= "10437" (postal-code address))) 135 | (is (= "Berlin" (city address))) 136 | (is (= "Berlin" (region address))) 137 | (let [country (country address)] 138 | (is (= "de" (:iso-3166-1-alpha-2 country))) 139 | (is (= "Germany" (:name country)))) 140 | (let [location (location address)] 141 | (is (approx= 52.54258 (:lat location))) 142 | (is (approx= 13.42299 (:lng location)))))))) 143 | -------------------------------------------------------------------------------- /test/geocoder/maxmind_test.clj: -------------------------------------------------------------------------------- 1 | (ns geocoder.maxmind-test 2 | (:require [clojure.test :refer :all] 3 | [geocoder.maxmind :as geocoder])) 4 | 5 | (def ip-address "92.229.192.11") 6 | 7 | (def db (geocoder/db geocoder/default-path)) 8 | 9 | (deftest test-geocode-ip-address 10 | (is (nil? (geocoder/geocode-ip-address db "127.0.0.1"))) 11 | (let [result (geocoder/geocode-ip-address db ip-address)] 12 | (is (= nil (:city result))) 13 | (is (= "Germany" (:name (:country result)))) 14 | (is (= "de" (:iso-3166-1-alpha-2 (:country result)))) 15 | (is (number? (:lat (:location result)))) 16 | (is (number? (:lng (:location result)))))) 17 | 18 | (deftest test-wrap-maxmind 19 | (let [response ((geocoder/wrap-maxmind identity geocoder/default-path) {:remote-addr ip-address})] 20 | (is (= (geocoder/geocode-ip-address db ip-address) (:maxmind response))))) 21 | -------------------------------------------------------------------------------- /test/geocoder/utils.clj: -------------------------------------------------------------------------------- 1 | (ns geocoder.utils 2 | (:require [clojure.test :refer :all])) 3 | 4 | (def tolerance 0.01) 5 | 6 | (defn approx= 7 | [a b] 8 | (<= (- a tolerance) b (+ a tolerance))) 9 | 10 | (deftest approx=-test 11 | (is (true? (approx= 1 (+ 1 tolerance)))) 12 | (is (false? (approx= 1 2)))) 13 | --------------------------------------------------------------------------------