Geo encoding a location Part 4

  • Antony Pinchbeck
  • 2020-09-12
  • Elixir

Project source code: https://github.com/antp/locations

Adding a map

First add Leaflet.js by following their installation instructions.

For the version used here, add the following to the root.html.leex LiveView template:

<link rel="stylesheet" href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css"
    integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ=="
    crossorigin="" />
<script src="https://unpkg.com/leaflet@1.6.0/dist/leaflet.js"
    integrity="sha512-gZwIG9x3wUXg2hdXF6+rVkLF/0Vi9U8D2Ntg4Ga5I5BZpVkVxlJWbSQtXPSiUTtC0TjtGOmxa1AJPuV0CPthew=="
    crossorigin=""></script>

The next issue is to actually display the map. The map can only be shown if we have the latitude and longitude of the location. After initially entering an address, they will be nil. The map should only be shown if they are available, otherwise a message stating the map cannot be displayed should be displayed.

The code for these modules can be found in lib\locations_web\live\location\show_location.(ex | html.leex).

<%= if @location.lat do %>
<!— display the map —> 
<% else %>
  <p class="p-4 text-lg text-center text-orange-600">
    Sorry we are unable to display the map
  </p class="p-4">
  <p>Please press the "Get Map" button if you want to display a map for this location.</p>
<% end %>

Javascript is used initialise leaflet allowing it to display the map. As the application supports multiple maps on a page, someway of telling the javascript which map it should be working with is required. Using LiveView hooks, an element in the UI can run javascript under certain conditions. In this case, when mounted or updated. The javascript can access the child elements from the element it is attached to, enabling a mechanism to pass information from the server to the javascript.

<div class="px-4 mt-0" phx-hook="AddMap">
    <input type="hidden" id="lat" name="lat" value=<%= @location.lat %>>
    <input type="hidden" id="lon" name="lon" value=<%= @location.lon %>>
    <input type="hidden" id="map_id" name="map_id" value=<%= get_map_id(@location) %>>

    <div id="<%= get_map_id(@location) %>"
            class="z-0 overflow-hidden border border-gray-500 rounded-lg map">
    </div>
</div>

In the above, the javascript hook AddMap is attached to the map display div. Within the div, the server passes the latitude, longitude and the ID of the element that leaflet should attach the map to. With this we can now uniquely identify a map on a page.

The get_map_id returns the ID of the location database entry. In this example, binary ID’s are being used, so this will be a UUID.

Adding the javascript hook

Fortunately the javascript is not to complicated. First query for the child elements to get the latitude, longitude and ID for the map. If they are all available, initialise the leaflet element with the latitude and longitude. Finally add a marker at the position so the user can see where it is.

const show_map = (el) => {
    const lat_el = el.querySelector('#lat')
    const lon_el = el.querySelector('#lon')
    const map_id_el = el.querySelector('#map_id')
    const map_id = '#' + map_id_el.value
    const map_el = el.querySelector(map_id)


    if (map_el) {
        // reset the map div
        map_el._leaflet_id = null;
    }

    if (map_el && lat_el && lat_el.value && lon_el && lon_el.value) {
        var lat = parseFloat(lat_el.value)
        var lon = parseFloat(lon_el.value)
        var mymap = L.map(map_el).setView([lat, lon], 17);

        L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
            attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
            subdomains: ['a', 'b', 'c']
        }).addTo(mymap);

        L.marker([lat, lon]).addTo(mymap);
    }
}

let Hooks = {}

Hooks.AddMap = {
    mounted() {
        show_map(this.el)

    },
    updated() {
        show_map(this.el)
    }
}

Multiple features

In the case where the address encodes to more than one feature, buttons are shown below the map. These allow the user to select their desired location.

<%= if :multiple_features == @geo_encoding_type do %>
    <div class="mt-2">
        <p class="text-center text-orange-600">Multiple locations found. Please select a venue.</p>
     </div>
     <%= for place <- @location.geo_features do %>
        <div class="mt-2">
            <button type="button" id="<%= get_geo_place_id_for_location(@location, place) %>"
                    class="p-2 map-button <%= get_geo_place_selected(place, @location.geo_selected_id) %> rounded"
                    phx-click="map_location_selected" phx-value-geo_id="<%= get_geo_place_id(place) %>"
                    phx-target="<%= @myself %>" phx-throttle="1000">
                    <%= get_geo_place_display_name(place) %><br />
                    (<%= get_geo_place_display_type(place) %>)
            </button>
        </div>
    <% end %>
<% end %>

Each of these buttons will send its unique osm_id to the map_location_selected function. This will update the locations latitude, longitude and geo_selected_id to reflect the change and redisplay the map.

Conclusion

The application should be straightforward to follow and has a series of unit tests for the location context and functional tests for the LiveView UI.

I hope the example application is useful to some in showing one way to encode and display maps in a Phoenix LiveView application.

© 2001 - 2020 Component X Software Limited All rights reserved.

Lynx: 2.4.5