Rendering a Google Map in a JavaScript Front-End: Part II
Storing and Rendering Points and Polygons on the map
In my last post, I discussed my approach to setting up a project on the Google Cloud Platform and rendering a map in a JavaScript frontend via the Google Maps JavaScript API. In this post, I’ll detail the approach that I took to generate new point and polygon data in a rendered map that persist both optimistically and pessimistically in a Ruby-on-Rails backend database.
The project that I created is a web tool to help firefighters and foresters track wildfires and plan prescribed burns. For my purposes, I wanted to give the end-user the ability to plot new point and polygon data that is editable, deletable, and persistent.
Backend
Creating a Rails API
To create persistent Google Maps point and polygon data, I first built a rails backend database and API to house the information and then retrieved the necessary data on the frontend via the API. I created a “points” model that included string/text fields for a title, description, link, as well as float fields for the point’s latitude and longitude:
create_table "points", force: :cascade do |t|
t.string "title"
t.float "latitude"
t.float "longitude"
t.string "link"
t.text "description"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
I also generated a “polygons” model with the same string and text fields. However, I included fifteen distinct float fields for both the latitudes and longitudes of potential polygon vertices. This was a slightly arbitrary choice that limits the new polygons created in the app to fourteen sides.
create_table "polygons", force: :cascade do |t|
t.string "title"
t.string "link"
t.string "description"
t.float "lat1"
t.float "long1"
t.float "lat2"
t.float "long2"
t.float "lat3"
t.float "long3" ... t.float "lat15"
t.float "long15"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
Additionally, I created controller files for both the point and polygon models that include relevant CRUD methods and render the resulting JSON data to the Rails API.
class PointsController < ApplicationController
def index
points = Point.all
render json: points, except: [:updated_at, :created_at]
enddef create
point = Point.create(title: params[:title], latitude:
params[:latitude], longitude: params[:longitude], link:
params[:link], description: params[:description])
render json: point, except: [:updated_at, :created_at]
enddef show
point = Point.find_by(id: params[:id])
render json: point, except: [:updated_at, :created_at]
enddef update
point = Point.find_by(id: params[:id])
point.update(point_params)
render json: point, except: [:updated_at, :created_at]
enddef destroy
point = Point.find_by(id: params[:id])
point.delete
endprivatedef point_params
params.require(:point).permit(:id, :title, :latitude,
:longitude, :description, :link)
end
end
For my purposes, running a local Rails server for frontend data retrieval was sufficient.
Frontend
Creating New Point Data
The creation of a new data point should occur when a user interacts with the map in a predetermined way. The Google Maps JavaScript API has several built-in event listeners (e.g. click, double click, mouseover) that can trigger a method(s) upon server-side completion of the selected action. I built an event listener that responds to a mouse double click on the map display by creating a new data point.
google.maps.event.addListener(map, 'dblclick', function(event){
})
Within the event listener, I create a new Google Maps marker with a position set to the latitude and longitude of the click event.
let marker = new google.maps.Marker({
position: event.latLng,
map: map,
draggable: true
})
Optionally, you can create a new info window associated with each new point that opens upon creation. I also created a blank ‘div’ element as placeholder content within the info window.
let div = document.createElement('div')let infoWindow = new google.maps.InfoWindow({
content: div
})
infoWindow.open(map, marker)
Within the event listener, I also created a form that allows for user input associated with each new point when stored in the backend database. The form includes a series of input fields to collect the relevant point information, stored as innerHTML.
let form = document.createElement('form')
form.innerHTML =
` <div class='new-Form'>
<form>
<h3> Create New Point </h3>
<input type="hidden" id="id" name="id" value="id">
Title: <input type="text" id="title" name="title" value="">
Description: <inputtype="textContent" id="description" name="description" value="">
Latitude: <input id="latitude" name="latitude" value="${event.latLng.lat()}">
Longitude: <input id="longitude" name="longitude" value="${event.latLng.lng()}">
Link: <input type="text" id="link" name="link" value="">
</form>
</div> `
Additionally, I created a form submit button with a click event that triggers a subsequent “getFormData” function on click (discussed later). I then appended both the form and submit button to the ‘div’ element contained as content within each new info window.
let submitButton = document.createElement('button')
submitButton.innerText = 'Create New Point'
submitButton.addEventListener('click', function(event){
event.preventDefault()
getFormData()
})div.append(form, submitButton)
On double click and creation of a new point, an info window complete with the aforementioned form and submit button should open and allow for user input.
Upon submission of the info window form, I needed to collect the data input by the user for use in later methods. I did this by calling a “getFormData” function that utilized the JavaScript FormData constructor to retrieve each user input and store it to a variable.
function getFormData(){
let formData = new FormData(form)
let title = formData.get('title')
let description = formData.get('description')
let latitude = formData.get('latitude')
let longitude = formData.get('longitude')
let link = formData.get('link')addNewPoint(title, description, latitude, longitude, link)
}
“getFormData” calls on an additional function, “addNewPoint,” which receives the user input data and triggers an HTTP POST request to the established API and adds a new entry to the backend database.
function addNewPoint(title, description, latitude, longitude, link){
marker.setMap(null)let clickconfig = {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},
body: JSON.stringify({
title,
description,
latitude,
longitude,
link
})
}fetch(POINTS_URL, clickconfig)
.then(response => response.json())
.then(renderPointCard)
.catch(function(error){
console.log(error.message)
})
}
})
This function submits the serialized user data to the API and parses the resulting promise as JSON. I then pass the resulting data to an additional function, “renderPointCard,” which creates additional styling and click functionality on the new point data but is outside the scope of this post.
Upon refreshing the page, newly created points should persist on the page.
Creating New Polygon Data
The Google Maps JavaScript API has a built-in feature, Drawing Manager, that allows a user to easily draw new polygons (or circles, rectangles, etc.) on a map. I created a new Drawing Manager instance inside of my “initMap” function, set the position of the polygon selector button to the bottom center of the map, and customized the styling of polygon presentation.
let drawingManager = new google.maps.drawing.DrawingManager({
drawingMode: null,
drawingControl: true,
drawingControlOptions: {
position: google.maps.ControlPosition.BOTTOM_CENTER,
drawingModes: ['polygon']
},
polygonOptions: {
fillColor: '#ff5733',
fillOpacity: 0.3,
strokeWeight: 1,
clickable: true,
editable: true,
zIndex: 1,
}
});
drawingManager.setMap(map)
As I described above for creating a new data point, I similarly created a map-level event listener that is triggered by the completion of a Drawing Manager polygon.
google.maps.event.addListener(drawingManager, 'polygoncomplete', function(polygon){
}
Upon completion, the polygon’s data are passed through a larger function that identifies the latitudes and longitudes of each polygon vertex. I accomplished this by using the Google Maps API’s built-in “getPaths()” method to identify an object of all latLngs that comprise the polygon and storing the resulting values to a variable. The polygon feature is a nested object that requires some drilling to identify individual latitudes and longitudes (note the .g chains below). Using a for-loop, I then separated individual latitudes and longitudes and stored them in a new array.
let p = polygon.getPaths("latLngs").g[0].glet latLng = []
for(let i = 0; i < p.length; i++){
let lat = 0
lat = p[i].lat()
let long = 0
long = p[i].lng()latLng.push(lat)
latLng.push(long)
}
I followed the same process detailed above to generate an info window for each new polygon. Each of these info windows contains an editable form and submit button that triggers a method to retrieve and store the data entered into the form by the user.
let div = document.createElement('div')let form = document.createElement('form')
form.innerHTML =
`<div>
<form>
<h3> Create New Polygon </h3>
<input type="hidden" id="id" name="id" value="id">
<input type="hidden" id="polygon_id" name="polygon_id" value="polygon_id">
<input type="hidden" id="lat1" name="lat1" value="${latLng[0]}"
<input type="hidden" id="lat2" name="lat2" value="${latLng[2]}">...<input type="hidden" id="lat15" name="lat15" value="${latLng[28]}"
<input type="hidden" id="long1" name="long1" value="${latLng[1]}">
<input type="hidden" id="long2" name="long2" value="${latLng[3]}">...
<input type="hidden" id="long15" name="long15" value="${latLng[29]}">Title: <input type="text" id="title" name="title" value="">
Description: <input type="textContent" id="description" name="description" value="">
Link: <input type="text" id="link" name="link" value=""></form>
</div>`let polySubmitButton = document.createElement('button')
polySubmitButton.innerText = 'Create New Wildfire'
polySubmitButton.addEventListener('click', function(event){
event.preventDefault()
getPolygonFormData()
})div.append(form, polySubmitButton)
In this instance, the individual latitude and longitude values for each polygon vertex are passed to the “getPolygonFormData” function via hidden form fields.
function getPolygonFormData(){
let formData = new FormData(form)
let title = formData.get('title')
let description = formData.get('description')
let link = formData.get('link')
let lat1 = formData.get('lat1')
let long1 = formData.get('long1')
let lat2 = formData.get('lat2')
let long2 = formData.get('long2')...let lat15 = formData.get('lat15')
let long15 = formData.get('long15')}
This function then passes Form Data to a subsequent function, “addNewPolygon,” which performs an HTTP POST request to the API and adds a new polygon entry to the backend database.
function addNewPolygon(
title,
description,
link,
lat1,
long1,
lat2,
long2,...
lat15,
long15
){let polyConfig = {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},
body: JSON.stringify({
title,
description,
link,
lat1,
long1,
lat2,
long2,...
lat15,
long15
})
}
fetch(POLYGONS_URL, polyConfig)
.then(response => response.json())
.then(renderPolygonCard)
.catch(function(error){
console.log(error.message)
})
}})
As with the point data methodology described above, this function submits the serialized user data to the API and parses the resulting promise as JSON. I then pass the resulting data to an additional function, “renderPolygonCard,” which creates additional styling and click functionality on the new point polygon data.
Upon refreshing the page, newly created polygons should also persist on the page.
For additional detail on how I enabled full CRUD methods on both point and polygon data using vanilla JavaScript, feel free to access my full GitHub repository, found here:
Contributions and comments are always welcome!