Geo encoding a location Part 4
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:
'© <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.