defmodule GeolocationClient do @moduledoc """ Client for Google Geolocation API """ @base_url "https://www.googleapis.com/" @doc """ Set API key """ @spec set_api_key(String.t()) :: :ok def set_api_key(key) do Application.put_env(:geolocation_client, :api_key, key) end @doc """ Geolocate from a list of MAC addresses """ @spec geolocate_from_mac_addresses([GeolocationClient.Entities.MacAddress.t()], boolean()) :: GeolocationClient.Entities.LatLon.t() | {:error, any()} def geolocate_from_mac_addresses(mac_addresses, consider_ip \\ false) when is_list(mac_addresses) do # Implementation would go here url = "#{@base_url}geolocation/v1/geolocate?key=#{Application.get_env(:geolocation_client, :api_key)}" body = %{ "considerIp" => consider_ip, "wifiAccessPoints" => Enum.map(mac_addresses, fn mac -> %{ "macAddress" => mac.mac_address, "signalStrength" => mac.signal_strength } end) } case Req.post(url, json: body) do {:ok, %Req.Response{status: 200, body: %{"location" => location, "accuracy" => accuracy}}} -> %GeolocationClient.Entities.LatLon{ latitude: location["lat"], longitude: location["lng"], accuracy: accuracy } {:ok, %Req.Response{status: status, body: body}} -> {:error, %{status: status, body: body}} {:error, reason} -> {:error, reason} end end @doc """ Get altitude for given latitude and longitude """ @spec get_altitude(GeolocationClient.Entities.LatLon.t()) :: float() | {:error, any()} def get_altitude(%GeolocationClient.Entities.LatLon{latitude: lat, longitude: lon}) do # Implementation would go here url = "https://maps.googleapis.com/maps/api/elevation/json?locations=#{lat}%2C#{lon}&key=#{Application.get_env(:geolocation_client, :api_key)}" case Req.get(url) do {:ok, %Req.Response{status: 200, body: %{"results" => [%{"elevation" => elevation} | _]}}} -> elevation {:ok, %Req.Response{status: status, body: body}} -> {:error, %{status: status, body: body}} {:error, reason} -> {:error, reason} end end @doc """ Geolocate from mac addresses and get full location (LLA) """ @spec geolocate_full_location([GeolocationClient.Entities.MacAddress.t()], boolean()) :: GeolocationClient.Entities.Location.t() | {:error, any()} def geolocate_full_location(mac_addresses, consider_ip \\ false) when is_list(mac_addresses) do case geolocate_from_mac_addresses(mac_addresses, consider_ip) do %GeolocationClient.Entities.LatLon{latitude: lat, longitude: lon} = lat_lon -> case get_altitude(lat_lon) do elevation when is_float(elevation) -> %GeolocationClient.Entities.Location{ latitude: lat, longitude: lon, altitude: elevation } {:error, _} = error -> error end {:error, _} = error -> error end end @doc """ Reverse geocode from latitude and longitude to address """ @spec reverse_geocode(GeolocationClient.Entities.LatLon.t()) :: GeolocationClient.Entities.Geocode.t() | {:error, any()} def reverse_geocode(%GeolocationClient.Entities.LatLon{latitude: lat, longitude: lon}) do url = "https://maps.googleapis.com/maps/api/geocode/json?latlng=#{lat},#{lon}&key=#{Application.get_env(:geolocation_client, :api_key)}" case Req.get(url) do {:ok, %Req.Response{status: 200, body: %{"results" => [result | _]}}} -> address_components = result["address_components"] || [] # Create the Geocode struct with all possible address components %GeolocationClient.Entities.Geocode{ street_number: find_component(address_components, "street_number"), street_address: find_component(address_components, "street_address"), route: find_component(address_components, "route"), intersection: find_component(address_components, "intersection"), political: find_component(address_components, "political"), country: find_component(address_components, "country"), administrative_area_level_1: find_component(address_components, "administrative_area_level_1"), administrative_area_level_2: find_component(address_components, "administrative_area_level_2"), administrative_area_level_3: find_component(address_components, "administrative_area_level_3"), administrative_area_level_4: find_component(address_components, "administrative_area_level_4"), administrative_area_level_5: find_component(address_components, "administrative_area_level_5"), administrative_area_level_6: find_component(address_components, "administrative_area_level_6"), administrative_area_level_7: find_component(address_components, "administrative_area_level_7"), colloquial_area: find_component(address_components, "colloquial_area"), locality: find_component(address_components, "locality"), sublocality: find_component(address_components, "sublocality"), sublocality_level_1: find_component(address_components, "sublocality_level_1"), sublocality_level_2: find_component(address_components, "sublocality_level_2"), sublocality_level_3: find_component(address_components, "sublocality_level_3"), sublocality_level_4: find_component(address_components, "sublocality_level_4"), sublocality_level_5: find_component(address_components, "sublocality_level_5"), neighborhood: find_component(address_components, "neighborhood"), premise: find_component(address_components, "premise"), subpremise: find_component(address_components, "subpremise"), plus_code: find_component(address_components, "plus_code"), postal_code: find_component(address_components, "postal_code"), natural_feature: find_component(address_components, "natural_feature"), airport: find_component(address_components, "airport"), park: find_component(address_components, "park"), point_of_interest: find_component(address_components, "point_of_interest"), formatted_address: result["formatted_address"] } {:ok, %Req.Response{status: status, body: body}} -> {:error, %{status: status, body: body}} {:error, reason} -> {:error, reason} end end # Find address components by type - response lists types per component defp find_component(address_components, type) do component = Enum.find(address_components, fn component -> Enum.member?(component["types"], type) end) case component do nil -> nil _ -> component["long_name"] end end end