Create a filemaker app that integrates google map api for drawing regions and testing coordinates.
A filemaker app that integrates google map api for drawing regions and testing coordinates. We will create this from scratch and hope you follow along.
------------WHAT THE APP DOES---------------------
Draw as many regions/areas as you want on the map using Google Maps polygons control and remember to name them. You can also edit the names of your areas in the "region" layout. These polygons will be used to check which coordinate belongs to which area on the map.
Enter your coordinates into the longitude and latitude fields in the region layout. Remember to use the "New Record" button.
You can also check and edit your coordinates from the dataLocation layout. You can as well import your coordinate into those fields, as you wish...
When your polygons and coordinate data are ready. Click the Start button IN THE REGION LAYOUT (Note: there are two start buttons. Use the one in "region" layout to send your data to the webviewer...
Then finally move to the "map" layout. This is where your map is. Click the start button here to compute areas. Areas will be computed for each coordinate and the result will be stored in the dataLocation layout table in JSON format, because a coordinate has many to many relationship with your areas or polygon... It will be easier if there is a one to one relationship though.
Note: Your coordinates data will be used to create markers for visual feedback so you can easily locate them and where they belong on the map.
You can also change the api key from the map layout...
You can also export your data to an external json file by running the appropriate scripts...from the script workspace.
----------FOLLOW THE INSTRUCTION TO CREATE THE APP--------------
PREREQUISITE:
TOOLS:
1. CREATE YOUR DATABASE AND TABLES:
In your local database, create two tables and name them anything you wish. In my own case, I named them: location and region.
In the location table, we will store majorly the coordinates information such as latitude and longitude data that we need to check. We would also leave a field for the areas they belong, however, we will not enter anything in this field directly because the result of this field will be calculated by our app. We can also enforce this from the field settings.
In the region table, we will have the names of each region we create using the Google Map's polygon control. More so, we will also create a field to store the coordinates of the vertices of our polygons commonly referred to as the polygon's paths.
You may be tempted to store the polygons directly rather than their paths. Well, it should be possible because an attempt to store the polygons in an array was successful, however, I find it to be a lot more efficient to just pass many polygon's data back and forth from Javascript to Filemaker through JSON, hence just one parameter that refer to the JSON, so I decided to JSON-encode the polygon's paths data, which is impossible with an MVCArray, i.e. the polygon object native type.
We will also have some global fields that we will use to pass our JSON data to the webviewer that harbors our map for region calculations. And maybe another one to hold our API key.
Finally on this part, since the area names will be the final output that will be written in the location's table, and since this same area names are the names also entered into the region's table from the polygons created directly in the map's layout, then we can create a relationship between our two tables using their region/area fields.
2. CREATE YOUR LAYOUTS:
Create three layouts. Name them anything you want as usual. However, I named mine: map, location and region. More so, it does not necessarily have to be like that. You can decide how you want to create yours.
The map layout will contain the map where polygons will be drawn. I decided to make it a full screen map so you have the full canvas for drawing polygons. Makes sense right?
The location layout will consist of the location table. There, you can enter or import the coordinates you wish to test.
The region layout will contain the polygon's data such as names and paths information. You may also have your global fields there and possibly resulting JSON data fields
3. CREATE YOUR SCRIPTS:
Now that your database and layouts are ready the next thing to do is create your scripts that passes your data back and forth and also do the calculations.
The first thing you want to do is create the javascript that will create the polygons, name them and pass the information to your filemaker app.
To do that, you need to create an HTML, CSS and Javascript program. The easiest way is to enter your program directly into the FileMaker WebViewer control.
However, I find it easy to test my JavaScript code out of FileMaker first before transferring it into FileMaker WebViewer. Following this approach, create a separate file and name it anything you want, I'll name mine "google.html".
Inside google.html, write or copy the following html code as a template:
<!DOCTYPE html>
<html>
<head>
</head>
<body>
</body>
</html>
The first thing to do is add google map's dependency via the style tag as usual, after which, you will need to define the size of your map through CSS:
<!DOCTYPE html>
<html>
<head>
<style>
/* Set the size of the div element that contains the map */
#map {
height: 400px; /* The height is 400 pixels */
width: 100%; /* The width is the width of the web page */
}
</style>
<script defer src='https://maps.googleapis.com/maps/api/js?key=AIzaSyCbO_rYdIovGZDxIvqaonxXGKf0Q6ErRuc&callback=initMap&libraries=drawing,geometry'>
</script>
</head>
<body>
</body>
</html>
Please note that I used "/* */" multi-line comment delimiter as opposed to "//" for single line comment, as this works better for Filemaker in general. Remember that we intend to transfer this code to FileMaker.
Also note that we have added our API key, and both the drawing and geometry libraries to our google map script dependencies:
<script defer src='https://maps.googleapis.com/maps/api/js?key=AIzaSyCbO_rYdIovGZDxIvqaonxXGKf0Q6ErRuc&callback=initMap&libraries=drawing,geometry'>
The added callback (initMap) is the first function that will be called by default. It is added by default. Having done that, we shall add out elements. We need two elements:
1. A map element
2. A navigation component where we shall first hardcode the input box of our app to get latitude and longitude coordinates from the user. These coordinates will be used to check for which region a particular geographical point belongs. Remember we'll draw these polygons later on.
3. A submit button. This will be used to invoke the calculation function.
<!DOCTYPE html>
<html>
<head>
<style>
/* Set the size of the div element that contains the map */
#map {
height: 400px; /* The height is 400 pixels */
width: 100%; /* The width is the width of the web page */
}
</style>
<script defer src='https://maps.googleapis.com/maps/api/js?key=AIzaSyCbO_rYdIovGZDxIvqaonxXGKf0Q6ErRuc&callback=initMap&libraries=drawing,geometry'>
</script>
</head>
<body>
<h3>My Google Maps Demo</h3>
<nav>
<input id='lat' type='number' placeholder='Enter Latitude'>
<input id='lon' type='number' placeholder='Enter Longitude'>
<button id='submit'>Get Area</button>
</nav>
<!--The div element for the map -->
<div id='map' style="border: 1px solid red"></div>
<!DOCTYPE html>
<html>
<head>
<style>
/* Set the size of the div element that contains the map */
#map {
height: 400px; /* The height is 400 pixels */
width: 100%; /* The width is the width of the web page */
}
</style>
<script defer src='https://maps.googleapis.com/maps/api/js?key=AIzaSyCbO_rYdIovGZDxIvqaonxXGKf0Q6ErRuc&callback=initMap&libraries=drawing,geometry'>
</script>
</head>
<body>
<h3>My Google Maps Demo</h3>
<nav>
<input id='lat' type='number' placeholder='Enter Latitude'>
<input id='lon' type='number' placeholder='Enter Longitude'>
<button id='submit'>Get Area</button>
</nav>
<!--The div element for the map -->
<div id='map' style="border: 1px solid red"></div>
<script>
var polyarray = [];
function initMap() {
const map = new google.maps.Map(document.getElementById('map'), {
center: { lat: -34.397, lng: 150.644 },
zoom: 8,
});
</script>
</body>
</html>
Notice the polyarray variable, i used it to hold the polygons drawn in main memory. You can load your map now (You may decide to load it through a local server if you wish) from the browser.
Since we will use the drawing library to define the our regions, it is time to invoke and configure it using the inbuilt drawing manager as shown:
<!DOCTYPE html>
<html>
<head>
<style>
/* Set the size of the div element that contains the map */
#map {
height: 400px; /* The height is 400 pixels */
width: 100%; /* The width is the width of the web page */
}
</style>
<script defer src='https://maps.googleapis.com/maps/api/js?key=AIzaSyCbO_rYdIovGZDxIvqaonxXGKf0Q6ErRuc&callback=initMap&libraries=drawing,geometry'>
</script>
</head>
<body>
<h3>My Google Maps Demo</h3>
<nav>
<input id='lat' type='number' placeholder='Enter Latitude'>
<input id='lon' type='number' placeholder='Enter Longitude'>
<button id='submit'>Get Area</button>
</nav>
<!--The div element for the map -->
<div id='map' style="border: 1px solid red"></div>
<script>
var polyarray = [];
function initMap() {
const map = new google.maps.Map(document.getElementById('map'), {
center: { lat: -34.397, lng: 150.644 },
zoom: 8,
});
const drawingManager = new google.maps.drawing.DrawingManager({
drawingMode: google.maps.drawing.OverlayType.POLYGON,
drawingControl: true,
drawingControlOptions: {
position: google.maps.ControlPosition.TOP_CENTER,
drawingModes: [
google.maps.drawing.OverlayType.MARKER,
google.maps.drawing.OverlayType.CIRCLE,
google.maps.drawing.OverlayType.POLYGON,
google.maps.drawing.OverlayType.POLYLINE,
google.maps.drawing.OverlayType.RECTANGLE
],
},
markerOptions: {
icon: 'https://developers.google.com/maps/documentation/javascript/examples/full/images/beachflag.png',
},
});
drawingManager.setMap(map);
}
</script>
</body>
</html>
For more information on the configuration options, visit the google maps API docs. Now you can draw on your map via four modes:
1. You can insert markers
2. You can draw lines
3. You can draw circles and
4. You can draw polygons. In this app, we only need the polygon and marker modes, so you can remove the other two.
We want to invoke a save function each time a polygon is drawn so we can use its data in our calculation. Also, you may need to know what coordinate a point has for testing purpose as we would when testing our app. I have included to functions. One to shout out the drawn marker coordinates and the other to prompt the user for the name of the polygon or region:
<!DOCTYPE html>
<html>
<head>
<style>
/* Set the size of the div element that contains the map */
#map {
height: 400px; /* The height is 400 pixels */
width: 100%; /* The width is the width of the web page */
}
</style>
<script defer src='https://maps.googleapis.com/maps/api/js?key=AIzaSyCbO_rYdIovGZDxIvqaonxXGKf0Q6ErRuc&callback=initMap&libraries=drawing,geometry'>
</script>
</head>
<body>
<h3>My Google Maps Demo</h3>
<nav>
<input id='lat' type='number' placeholder='Enter Latitude'>
<input id='lon' type='number' placeholder='Enter Longitude'>
<button id='submit'>Get Area</button>
</nav>
<!--The div element for the map -->
<div id='map' style="border: 1px solid red"></div>
<script>
var polyarray = [];
function initMap() {
const map = new google.maps.Map(document.getElementById('map'), {
center: { lat: -34.397, lng: 150.644 },
zoom: 8,
});
const drawingManager = new google.maps.drawing.DrawingManager({
drawingMode: google.maps.drawing.OverlayType.POLYGON,
drawingControl: true,
drawingControlOptions: {
position: google.maps.ControlPosition.TOP_CENTER,
drawingModes: [
google.maps.drawing.OverlayType.MARKER,
/* google.maps.drawing.OverlayType.CIRCLE,*/
google.maps.drawing.OverlayType.POLYGON
/*google.maps.drawing.OverlayType.POLYLINE,*/
/*google.maps.drawing.OverlayType.RECTANGLE,*/
],
},
markerOptions: {
icon:
'https://developers.google.com/maps/documentation/javascript/examples/full/images/beachflag.png',
},
});
google.maps.event.addListener(drawingManager, 'markercomplete', function(marker) {
alert(marker.getPosition().lat() + ' ' + marker.getPosition().lng());
});
google.maps.event.addListener(drawingManager, 'polygoncomplete', function(polygon) {
var areaName = prompt('Enter Name of Area');
let polyWithName = {polyObj:polygon, polyName:areaName};
polyarray.push(polyWithName);
});
drawingManager.setMap(map);
}
</script>
</body>
</html>
Lastly, we want to invoke our calculation function when the submit button is clicked. The callback function checks each polygon against the coordinate through the inbuilt poly.containsLocation( ) method.
<!DOCTYPE html>
<html>
<head>
<style>
/* Set the size of the div element that contains the map */
#map {
height: 400px; /* The height is 400 pixels */
width: 100%; /* The width is the width of the web page */
}
</style>
<script defer src='https://maps.googleapis.com/maps/api/js?key=AIzaSyCbO_rYdIovGZDxIvqaonxXGKf0Q6ErRuc&callback=initMap&libraries=drawing,geometry'>
</script>
</head>
<body>
<h3>My Google Maps Demo</h3>
<nav>
<input id='lat' type='number' placeholder='Enter Latitude'>
<input id='lon' type='number' placeholder='Enter Longitude'>
<button id='submit'>Get Area</button>
</nav>
<!--The div element for the map -->
<div id='map' style="border: 1px solid red"></div>
<script>
var polyarray = [];
function initMap() {
const map = new google.maps.Map(document.getElementById('map'), {
center: { lat: -34.397, lng: 150.644 },
zoom: 8,
});
const drawingManager = new google.maps.drawing.DrawingManager({
drawingMode: google.maps.drawing.OverlayType.POLYGON,
drawingControl: true,
drawingControlOptions: {
position: google.maps.ControlPosition.TOP_CENTER,
drawingModes: [
google.maps.drawing.OverlayType.MARKER,
/* google.maps.drawing.OverlayType.CIRCLE,*/
google.maps.drawing.OverlayType.POLYGON
/*google.maps.drawing.OverlayType.POLYLINE,*/
/*google.maps.drawing.OverlayType.RECTANGLE,*/
],
},
markerOptions: {
icon:
'https://developers.google.com/maps/documentation/javascript/examples/full/images/beachflag.png',
},
});
google.maps.event.addListener(drawingManager, 'markercomplete', function(marker) {
alert(marker.getPosition().lat() + ' ' + marker.getPosition().lng());
});
google.maps.event.addListener(drawingManager, 'polygoncomplete', function(polygon) {
var areaName = prompt('Enter Name of Area');
let polyWithName = {polyObj:polygon, polyName:areaName};
polyarray.push(polyWithName);
});
document.getElementById('submit').addEventListener('click', function() {
var lat = document.getElementById('lat').value;
var lon = document.getElementById('lon').value;
if(lat <= 90 && lat >= -90 && lon <= 180 && lon >= -180) {
var testPos = new google.maps.LatLng(lat, lon);
var marker = new google.maps.Marker({
position: testPos,
map:map,
draggable: true,
animation: google.maps.Animation.DROP,
});
var abc = [];
polyarray.forEach(p => {
var polygon = p.polyObj
if (google.maps.geometry.poly.containsLocation(testPos, polygon)) {
abc.push(p.polyName);
}
});
setTimeout(
function(){
if (abc.length < 1) {
alert('No Area');
} else {
alert(abc);
}
}, 3000);
} else {
alert('Error data');
}
});
drawingManager.setMap(map);
}
</script>
</body>
</html>
What we did was get the coordinates from our input boxes and then used them to create a latlng object which is needed to create a marker (Creating a marker is not necessary here, we only used it for visually identifying our points). We then queried out google map API to inform us if the coordinates is contained in any of the polygon (A point can be contained in many polygon and vice versa, making it a many-to-many relationship). We finally waited sometime for the query to complete using the setTimeout( ) function since the polygons to check may be many. You can adjust your time to accommodate for the number of polygons you may be querying against. In my own case, I used 3 seconds because I have few polygons. Using setTimeout to wait synchronuously is not a good approach, we will therefore change this approach to a better one shortly.
Also note that we have constrained the ranges of our coordinates using an if () statement. We want to ensure that latitude is within the range of -90 to 90 and longitude within -180 to 180.
You can now test your program, it should be working fine. If you have any error, make sure you have followed the procedure properly.
The only downside is that you will lose all your work if you close and reopen your project, since all our data were stored in the volatile RAM. To correct this, we need to have a database, which in this case is obviously our FileMaker. Some changes will be made to our app in order to store our polygon data and resulting calculation data on Filemaker.
Copy and paste or type the following into your webviewer address box:
"data:text/html,<!DOCTYPE html>
<html>
<head>
<style>
/* Set the size of the div element that contains the map */
#map {
height: 400px; /* The height is 400 pixels */
width: 100%; /* The width is the width of the web page */
}
</style>
<script defer src='https://maps.googleapis.com/maps/api/js?key=" & stef::apikey & "&callback=initMap&libraries=drawing,geometry'>
</script>
</head>
<body>
<nav>
<button id='submit'>START</button>
</nav>
<!--The div element for the map -->
<div id='map'></div>
<script>
var polyarray = [];
var r = 0;
function initMap() {
const map = new google.maps.Map(document.getElementById('map'), {
center: { lat: -34.397, lng: 150.644 },
zoom: 8,
});
const drawingManager = new google.maps.drawing.DrawingManager({
drawingMode: google.maps.drawing.OverlayType.POLYGON,
drawingControl: true,
drawingControlOptions: {
position: google.maps.ControlPosition.TOP_CENTER,
drawingModes: [
google.maps.drawing.OverlayType.MARKER,
google.maps.drawing.OverlayType.POLYGON
],
},
markerOptions: {
icon:
'https://developers.google.com/maps/documentation/javascript/examples/full/images/beachflag.png',
},
});
google.maps.event.addListener(drawingManager, 'markercomplete', function(marker) {
alert(marker.getPosition().lat() + ' ' + marker.getPosition().lng());
});
google.maps.event.addListener(drawingManager, 'polygoncomplete', function(polygon) {
areaName = prompt('Enter Name of Area');
var p = polygon.getPath().getArray();
var pl = [];
p.forEach(function(pb) {
pc = pb.lat();
pd = pb.lng();
pe = {lat:pc, lng:pd};
pl.push(pe);
});
let polyWithName = {polyPath:pl, polyName:areaName};
let pa = JSON.stringify(polyWithName);
theURL = 'fmp://$/" & Get ( FileName ) & "?script=updatedb¶m=' + pa;
window.location = theURL ;
});
drawingManager.setMap(map);
var pod = " & region::regionJson & " ;
var l = 0;
var color = ['#ff4000', '#ffff00', '#4000ff', '#ffbf00', '#bfff00', '#00ffff', '#FFA07A', '#9ACD32'];
if (pod != null && pod !== undefined && pod.length > 0) {
pod.forEach(function(po) {
const pgon = new google.maps.Polygon({
paths: po.polyObj.polyPath,
fillColor: color[l],
map: map
});
if (l >= color.length - 1) {l = 0;} else {
l++;
}
});
}
}
document.getElementById('submit').addEventListener('click', function(e) {
var polyData = " & region::regionJson & " ;
var coordData = " & region::coordJson & " ;
if (polyData.length == 0) {alert('Wrong Data!'); return;}
if (coordData.length == 0) {alert('Wrong Data!'); return;}
var totalResult = [];
coordData.forEach(function(c) {
let lat = c.lat;
let lon = c.lon;
var coordResult = [];
if(lat <= 90 && lat >= -90 && lon <= 180 && lon >= -180) {
var latlon = new google.maps.LatLng(lat, lon);
var marker = new google.maps.Marker({
position: latlon,
map: map,
draggable: false,
animation: google.maps.Animation.DROP,
});
var i = 0;
polyData.forEach(function(po) {
const pgon = new google.maps.Polygon({
paths: po.polyObj.polyPath,
map: map
});
if (google.maps.geometry.poly.containsLocation(latlon, pgon)) {
var polyResult = {area: po.area};
coordResult.push(polyResult);
if (i === polyData.length-1) {i = 0; cont(coordResult, totalResult, coordData);} else { i++;}
} else {
var polyResult = {area: 'No Area'};
coordResult.push(polyResult);
if (i === polyData.length-1) {i = 0; cont(coordResult, totalResult, coordData);} else { i++;}
}
});
}
});
});
function cont(coordResult, totalResult, coordData) {
var cr = [];
for(var i = 0; i <= coordResult.length - 1; i++) {
if (coordResult[i].area !== 'No Area') {
cr.push(coordResult[i]);
}
}
if(cr.length !== 0) {
var polyResult = {area: cr};
totalResult.push(polyResult);
if (r === coordData.length-1) {
r = 0;
cont2(totalResult);
} else { r++;}
} else {
var polyResult = {area: 'No Area'};
totalResult.push(polyResult);
if (r === coordData.length-1) {r = 0; cont2(totalResult);} else { r++;}
}
}
function cont2(totalResult) {
var pz = JSON.stringify(totalResult);
theURL = 'fmp://$/" & Get ( FileName ) & "?script=updateresult¶m=' + pz;
window.location = theURL ;
}
</script>
</body>
</html>"
All the things to note have been punched out by boldening. The First thing to note here is the "data:text/html," which is needed to inform FileMaker that you wish to load an html. The next is the "layout::apikey". I added a global apikey field to my layout table and referred to it using "layout::apikey" which is one way to refer to the data in a field. The idea is to store the your Google Map API key in this one (global) field so it can be changed by the user or used by many independent user.
We have also removed our input elements so we can store our coordinates directly in Filemaker. Like that, we have opportunities, for instance to save our coordinates for future use, etc. You can also store many coordinates in a table and use them in your calculations all at once. Great isn't it?! We left the start button to invoke our calculation JavaScript.
Also, since there is a general prejudice towards sending JSON from JavaScript frontends to servers and databases and vice versa, we will be using this approach as opposed to sending an array of data which has known issues. However, because we cannot JSONize (I mean serialize) polygon data directly, we used "polygon.getPath().getArray()" to get the needed path of each polygon, and then the coordinates of the vertices of the polygon using lat( ) and lng( ), and then send those to FileMaker instead, such that whenever we need the associated polygon, we reconstructed it using the path and latlng object.
We used
theURL = 'fmp://$/" & Get ( FileName ) & "?script=updatedb¶m=' + pa;
window.location = theURL ;
to pass the the serialized data to FileMaker. For more information on that, check FileMaker specs.















Comments
Post a Comment