Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Freehand Drawing using Google Map and Spatial Search

0.00/5 (No votes)
15 Mar 2021 1  
How to use Google map and Spatial search to do freehand drawing
In this tip, you will learn how to perform a freehand drawing using Google maps and apply this drawing to a SQL search.

Using the Code

We will use JavaScript to perform a freehand drawing on Google maps. Then, we will pass the drawing information to a controller and perform Spatial search using Linq to SQL.

First, we will add this HTML on the page:

<div>
     <input id="DrawOnMap" type="checkbox">
     <label for="DrawOnMap"><span>Draw on Map</span></label>
    </div>
    <div id="DrawOnMapSearch">
      <p>Draw around the area you would like to search</p>
      <input id="DrawOnMapApply" value="Apply" type="button" disabled="disabled">
      <input id="DrawOnMapReDraw" value="Redraw" type="button" disabled="disabled">
</div>
<div id="map_canvas"></div>

The map will be created and displayed on:

<div id="map_canvas"></div>

Let's talk about the JavaScript.

For this code, we will use the geometry library of Google maps, we will use this page like a base example:

But unfortunately, Google map only allows these options to draw:

{ 
  CIRCLE: "circle", 
  MARKER: "marker", 
  POLYGON: "polygon", 
  POLYLINE: "polyline", 
  RECTANGLE: "rectangle" 
}

For the FreeHand drawing, we will use a combination of POLYGON and POLYLINE.

This will be the JS that we are going to use:

var drawOnMap = document.getElementById("DrawOnMap");

var drawOnMapApply = document.getElementById("DrawOnMapApply");
var drawOnMapReDraw = document.getElementById("DrawOnMapReDraw");
MapDrawing = function () {

}

MapDrawing.prototype = {
    ActiveDrawing: function (isMobile) {
        var self = this;
        var deviceEvents = self.getDeviceEvent(isMobile);
        google.maps.event.addDomListener(self.map.getDiv(),
            deviceEvents.mousedown,
            function (e) {
                self.drawFreeHand(deviceEvents.mousedown,
                deviceEvents.mousemove, deviceEvents.mouseup, isMobile);
                self.cancelEvtNStopProp(e);
            });
    },

    getDeviceEvent: function (isMobileDevice) {
        var deviceEvents;
        if (isMobileDevice) {
            deviceEvents = {
                mousedown: 'touchstart',
                mousemove: 'mousemove',
                mouseup: 'mouseup'
            };
        } else {
            deviceEvents = {
                mousedown: 'mousedown',
                mousemove: 'mousemove',
                mouseup: 'mouseup'
            };
        }
        return deviceEvents;
    },

    enableDrawing: function () {
        var self = this;
        if (self.map !== null) {
            self.map.setOptions({
                draggable: false,
                zoomControl: false,
                scrollwheel: false,
                disableDoubleClickZoom: false,
                draggableCursor: 'crosshair',
                streetViewControl: false,
                overviewMapControl: false,
                mapTypeControl: false,
                scaleControl: false,
                rotateControl: false,
                fullscreenControl: false
            });
        }
    },

    disableDrawing: function () {
        var self = this;
        if (self.map !== null) {
            self.map.setOptions({
                draggable: true,
                zoomControl: true,
                scrollwheel: true,
                disableDoubleClickZoom: true,
                draggableCursor: null,
                streetViewControl: true,
                overviewMapControl: true,
                mapTypeControl: true,
                scaleControl: true,
                rotateControl: true,
                fullscreenControl: true
            });
        }
    },

    drawFreeHand: function (mousedown, mousemove, mouseup, isMobile) {
        var self = this;
        //the polygon
        var dOptions = {
            clickable: false,
            fillColor: '#BCDCF9',
            fillOpacity: 0.5,
            strokeWeight: 2,
            strokeColor: '#57ACF9',
            zIndex: 1
        };

        self.poly = new google.maps.Polyline(dOptions);
        self.poly.setMap(self.map);
        var events = google.maps.event;
        //move-listener
        var move = events.addListener(self.map, mousemove, function (e)
                   { self.poly.getPath().push(e.latLng); });

        events.addListenerOnce(self.map, mouseup, function (e) {

            events.removeListener(move);

            var path = self.poly.getPath();
            self.poly.setMap(null);

            self.poly = new google.maps.Polygon(dOptions);
            self.poly.setMap(self.map);
            self.poly.setPaths(path);

            events.clearListeners(self.map.getDiv(), mousedown);

            var bounds = new google.maps.LatLngBounds();

            var myCoordinates = [];
            for (var i = 0; i < path.getLength(); i++) {
                var xy = path.getAt(i);
                var lat = xy.lat();
                var lng = xy.lng();
                bounds.extend(xy);
                myCoordinates.push({ Longitude: lng, Latitude: lat });
            }

            var ne = bounds.getNorthEast();
            var sw = bounds.getSouthWest();

            var data = {
                minLat: sw.lat(),
                minLng: sw.lng(),
                maxLat: ne.lat(),
                maxLng: ne.lng(),
                geographies: myCoordinates
            };

            if (data.geographies.length === 0) {
                self.disableDrawing();
                self.ActiveDrawing(isMobile);
            } else {
                self.DrawOnMapCallBack(data);
            }
        });
    },

    DrawOnMapCallBack: function (data) {
        this.DrawData = data;
        drawOnMapApply.disabled = false;
    },
    //removeFreeHand
    remFreeHand: function (isMobile) {
        var self = this;
        var deviceEvents = self.getDeviceEvent(isMobile);
        if (typeof (google) !== "undefined") {
            var clearListeners = google.maps.event.clearListeners;
            clearListeners(self.map, deviceEvents.mousemove);
            clearListeners(self.map, deviceEvents.mouseup);
            clearListeners(self.map.getDiv(), deviceEvents.mousedown);
        }
    },

    cancelEvtNStopProp: function (event) {
        var ev = event ? event : window.event; //Moz support requires
                                               //passing the event argument manually
        //Cancel the event
        ev.cancelBubble = true;
        ev.returnValue = false;
        if (ev.stopPropagation) ev.stopPropagation();
        if (ev.preventDefault) ev.preventDefault();
    },

    init: function () {
        var self = this;

        var mapOptions = {
            zoom: 14,
            center: new google.maps.LatLng(30.3079827, -97.8934853),
            mapTypeId: google.maps.MapTypeId.ROADMAP,
            zoomControl: true,
            zoomControlOptions: {
                style: google.maps.ZoomControlStyle[true],
                //position: google.maps.ControlPosition[null]
            },
            disableDefaultUI: false,
            draggable: true,
            scrollwheel: true,
            disableDoubleClickZoom: false,
            panControl: true,
            streetViewControl: true,
            overviewMapControl: true,
            mapTypeControl: true,
            scaleControl: true,
            rotateControl: true,
            fullscreenControl: true
        };

        self.map = new google.maps.Map
                   (document.getElementById('map_canvas'), mapOptions);
    }
};

function initialize() {

    var mapDrawing = new MapDrawing();
    mapDrawing.init();

    drawOnMap.addEventListener("change", function () {

        if (drawOnMap.checked) {
            drawOnMapReDraw.disabled = false;
            mapDrawing.enableDrawing();
            mapDrawing.ActiveDrawing(false);
        } else {
            drawOnMapApply.disabled = true;
            drawOnMapReDraw.disabled = true;
            mapDrawing.disableDrawing();
        }
    });

    drawOnMapReDraw.addEventListener("click", function () {
        drawOnMapApply.disabled = true;
        if (typeof (mapDrawing.poly) !== "undefined") {
            mapDrawing.poly.setMap(null);
        }
        drawOnMapApply.disabled = true;
        mapDrawing.enableDrawing();
        mapDrawing.ActiveDrawing(false);
    });

    drawOnMapApply.addEventListener("click", function () {
        var data = mapDrawing.DrawData;
    });
}

google.maps.event.addDomListener(window, 'load', initialize);

Let's break it in small parts:

The first thing that we need to know will be the events that we are going to use, because on mobile, there is a small difference, on mobile, the mousedown event is call touchstart.

getDeviceEvent: function (isMobileDevice) {
    var deviceEvents;
    if (isMobileDevice) {
        deviceEvents = {
            mousedown: 'touchstart',
            mousemove: 'mousemove',
            mouseup: 'mouseup'
        };
    } else {
        deviceEvents = {
            mousedown: 'mousedown',
            mousemove: 'mousemove',
            mouseup: 'mouseup'
        };
    }
    return deviceEvents;
}

The drawing will occur when the user clicks and holds the house on the map, is necessary to remove the necessary items and control the maps, we can enable them again if we don't want to draw any more on the map. Also, we will change the cursor for cross during the drawing to make it easy to see and understand.

enableDrawing: function () {
     var self = this;
     if (self.map !== null) {
         self.map.setOptions({
             draggable: false,
             zoomControl: false,
             scrollwheel: false,
             disableDoubleClickZoom: false,
             draggableCursor: 'crosshair',
             streetViewControl: false,
             overviewMapControl: false,
             mapTypeControl: false,
             scaleControl: false,
             rotateControl: false,
             fullscreenControl: false
         });
     }
 },

 disableDrawing: function () {
     var self = this;
     if (self.map !== null) {
         self.map.setOptions({
             draggable: true,
             zoomControl: true,
             scrollwheel: true,
             disableDoubleClickZoom: true,
             draggableCursor: null,
             streetViewControl: true,
             overviewMapControl: true,
             mapTypeControl: true,
             scaleControl: true,
             rotateControl: true,
             fullscreenControl: true
         });
     }
 }

Here comes the trick, when the user clicks and holds the mouse, the mousedown event will be fired, inside of this event, we will have a listener for mousemove and using the POLYLINE, the code will draw lines following the mouse cursor. When the user releases the mouse, the event mouseup will be triggered and will close the draw using the POLYGON.

Know we can gather the coordinates of the drawing:

var bounds = new google.maps.LatLngBounds();

var myCoordinates = [];
for (var i = 0; i < path.getLength(); i++) {
    var xy = path.getAt(i);
    var lat = xy.lat();
    var lng = xy.lng();
    bounds.extend(xy);
    myCoordinates.push({ Longitude: lng, Latitude: lat });
}

var ne = bounds.getNorthEast();
var sw = bounds.getSouthWest();

var data = {
    minLat: sw.lat(),
    minLng: sw.lng(),
    maxLat: ne.lat(),
    maxLng: ne.lng(),
    geographies: myCoordinates
};

We are also going to use a google.maps.LatLngBounds, to create an imaginary rectangle around the drawing, this will help us to speed up the search on the Linq to SQL.

Here is a live example of the JavaScript and HTML:

Controller to Perform the Search

This is the class that I am going use to pass the information to the server.

[JsonObject]
[Serializable]
public class ApiGeographyInformation
{
    public decimal MinLat { get; set; }
    public decimal MinLng { get; set; }
    public decimal MaxLat { get; set; }
    public decimal MaxLng { get; set; }

    public List<ApiGeography> Geographies { get; set; }
}

[JsonObject]
[Serializable]
public class ApiGeography
{
    public decimal Latitude { get; set; }
    public decimal Longitude { get; set; }

    public override string ToString()
    {
        return Longitude + " " + Latitude;
    }
}

To make sure that drawing is a valid one, we are going to use this validation to check the first and last point.

private string ValidatePolygonIntersects(ApiGeographyInformation searchParams)
{
    if (searchParams.Geographies.Any())
    {
        var first = searchParams.Geographies.First();
        var last = searchParams.Geographies.Last();

        if (first.ToString() != last.ToString())
            searchParams.Geographies.Add(first);
    }

    var bboxWkt = $"POLYGON(({string.Join(" , ", searchParams.Geographies)}))";
    var result = GetValidGeography(bboxWkt);
    return result;
}

For the method, GetValidGeography, we have two options to validate the drawing, because there is a big difference on the results if the draw is performed starting from the left with one that is starting from the right. This is a small hack when the user start drawing to the right.

First Option

We can use the SQL server to perform this validation, but some of the SQL methods are only available starting from SQL 2014.

USE [master]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
Create PROCEDURE [dbo].[pr_GetValidGeography]
(
   @bboxWkt varchar(max)
) 
AS
Set Transaction Isolation Level Read Uncommitted

BEGIN
    DECLARE @g geography = geography::STGeomFromText(@bboxWkt, 4326).MakeValid(); 
    DECLARE @g2 geography = @g.ReorientObject().MakeValid(); 
    DECLARE @geographyReturn geography;
    IF(@g.STArea()> @g2.STArea())
        BEGIN
         SET @geographyReturn = @g2;
        END
    ELSE
        BEGIN
          SET @geographyReturn =  @g;
        END

    SELECT @geographyReturn.STAsText() AS 'geography'
END
public string GetValidGeography(string area)
{
    var dataTable = new DataTable();
    using (var connection = new SqlConnection(""))
    {
        var command = new SqlCommand("pr_GetValidGeography", connection)
                      { CommandType = CommandType.StoredProcedure };
        command.Parameters.Add(new SqlParameter
        {ParameterName = "@bboxWkt", DbType = DbType.String, Value = area});


        var dataAdapter = new SqlDataAdapter(command);

        connection.Open();
        dataAdapter.Fill(dataTable);
        connection.Close();

        var data = dataTable.Rows[0]["geography"].ToString();
        return data;
    }
}

Second Option

Using the library of Microsoft.SqlServer.DACFx, personally, I recommend this option, because is faster than the SQL one.

Install-Package Microsoft.SqlServer.DACFx
private string GetValidGeography(string area)
{
    var bboxWkt = SqlGeography.STGeomFromText
                  (new System.Data.SqlTypes.SqlChars(area), 4326);
    // suffix "d" on literals below optional but explicit

    var g = bboxWkt.MakeValid();
    var g2 = g.ReorientObject();


    var geographyReturn = g.STArea() > g2.STArea() ? g2 : g;
    return geographyReturn.ToString();
}

And finally, we just need to perform the search using Ling to SQL:

public void ApplyMapPolygon(ApiGeographyInformation @params)
{
    var area = ValidatePolygonIntersects(@params);

    var minLat = @params.MinLat;
    var minLng = @params.MinLng;
    var maxLat = @params.MaxLat;
    var maxLng = @params.MaxLng;
    var polygon = DbGeography.FromText(area, 4326);

    var filterL = new List<Locations>();

    var results = from c in filterL
                  where c.Latitude > minLat &&
                        c.Latitude < maxLat &&
                        c.Longitude > minLng &&
                        c.Longitude < maxLng
                  select c;

    results = from c in filterL
        let lng = c.Longitude
        let lat = c.Latitude
        let point = DbGeography.FromText("POINT(" + lng + " " + lat + ")", 4326)
        where polygon.Intersects(point)

        select c;
}

Extra Documentation

History

  • 15th February, 2021: Initial submission

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here