Virtual Earth & SQL Server 2008 – Part 4: Integrating SQL Server 2008 and Virtual Earth (6/6)

Inserting data into SQL Server 2008

In the previous parts we have first been looking on the database side and how we can use various mechanisms to load and extract data and we even implemented a geocoder in the database. Then we moved on and investigated various ways to retrieve data from SQL Server 2008 and visualize them in Virtual Earth. In this final part we will now focus on using Virtual Earth as a simple data editor which allows us to write data back to SQL Server 2008.

We will implement the data editor in form of a query builder. In the user interface we will have some drawing tools which allow us to draw points, lines, polygons and circles on the map and to store them temporarily in our browser window. Each of the temporarily stored geographies will have a checkbox next to it and when we check these boxes we will dynamically create a SQL statement which automatically determines the appropriate feature-type. The SQL-statement will be displayed in a textarea and we will be able to manually edit it.

Once we click on the image with the SQL Server logo we will create an AJAX-call which sends the SQL-statement via HTTP-Post request to a HTTP handler and this handler will actually execute the statement.

image

Let’s start with a fresh table in our SQL Server. We create a table with a column of type geography.

CREATE TABLE [FeatureDemo]
(
    [id] [int] IDENTITY(1,1) NOT NULL,
    [Geom] geography NULL,
    CONSTRAINT [PK_FeatureDemo] PRIMARY KEY CLUSTERED 
    (
     [id] ASC
    )
) ON [PRIMARY]

In our ASP.NET page we first add a new accordion pane. This pane first displays a notice which reminds you that drawings which shall be added as a geography data type to the database must be drawn counter-clockwise. It also hosts buttons to draw various types of geographies and textboxes which will display the latitude and longitude under the mouse-cursor.

In the “Geometry”-section it provides 2 links: delete all drawings and reset the SQL statement in the textarea. Finally there is an empty DIV-element in which we will temporarily store our drawing objects with a checkbox next to them:

<cc1:AccordionPane ID="paneQueryBuilder" runat="server">
    <Header>Query Builder</Header>
    <Content>
        <b>Drawing Tools</b><br /><br />
        <i>Please make sure, you draw the polygon counter-clockise</i><br /><br />
        <input id="btnPoint" type="button" value="Point" onclick="Draw('point')" style="width: 125px;" />
        <input id="btnPolyline" type="button" value="Polyline" onclick="Draw('polyline')" style="width: 125px;" /><br />
        <input id="btnPolygon" type="button" value="Polygon" onclick="Draw('polygon')" style="width: 125px;" />
        <input id="btnCircle" type="button" value="Circle" onclick="Draw('circle')" style="width: 125px" /><br /><br />
        <a style="font-family:Courier New">Lat:&nbsp;</a><input id="txtLat" type="text" disabled="disabled" style="width: 210px" class="TxtBox" /><br />
        <a style="font-family:Courier New">Lon:&nbsp;</a><input id="txtLon" type="text" disabled="disabled" style="width: 210px" class="TxtBox" /><br /><br />
        <b>Geometries</b><br />
        <a href='javascript:DeleteAll()'>Delete All&nbsp;</a>
        <a href='javascript:ResetQuery()'>Reset SQL-Query</a><br />
        <div id="divGeom"></div>
    </Content>
</cc1:AccordionPane>

We will also need some more DIV-elements for the interaction with our geographies as well as the visualization and the editing of our SQL statement. Below you find the ASP.NET sources for a textarea where the SQL statement will be displayed, a clickable image which allows us to execute the AJAX-call as well as 2 elements which display the length of polylines and polygons while we draw as well as a dialog which allows us to enter the title and description of a geography.

<textarea id="txtSQL" rows="2" style="position:absolute; left:300px; bottom:5px; width:300px; height:94px" ></textarea>
<img src="IMG/Katmai.png" alt="Execute SQL-Statement" style="position:absolute; bottom:5px; right:5px; cursor:pointer;" onclick="SqlInsert();"/>
<div id='divDistance' style="position:absolute; left:10px; top:800px; visibility:hidden;" class="TxtBox";></div>
<div id='divShapeInfo' style="position:absolute; left:10px; top:850px; visibility:hidden;" class="TxtBox";>
    <b><u>Please enter the details:</u></b><br /><br />
    <a>ID&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:&nbsp;</a><input id="txtShapeID" type="text" disabled="disabled" style="width: 150px" /><br />
    <a>Title&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:&nbsp;</a><input id="txtShapeTitle" type="text" style="width: 150px" /><br />
    <a>Description:&nbsp;</a><input id="txtShapeDetails" type="text" style="width: 150px" /><br /><br />
    <input id="btnShapeInfo" type="button" value="Set" onclick="SetInfo()" style="width: 260px"/><br />
</div>

If we click a button to draw a geography we change the cursor to a crosshair and attach an onclick- and an onmousemove-event to the map. While we move the mouse we continuously update the latitude and longitude in the textboxes on the accordion pane. When we click with the left mouse-button we add a point to our temporary geography. If the geography is a polyline or polygon we will also determine the length of the line. For a circle the value in the divDistance will be the radius.

image

To finish the drawing we will hit the right mouse-button. This will detach the map-events and also bring up the divShapeInfo so that we can enter a title and a description. The distance will automatically be added to the shape.

image

When we click on “Set” in the dialogue above we will add the geography to our divGeom in the accordion pane. Next to the geography we have a checkbox and a JavaScript-function will update the textarea txtSQL with a statement appropriate for the selected geometries. For example if we have a polygon the statement will reflect that, if we have 2 polygons the statement will show a MULTIPOLYGON and if we have 2 polygons and a point the statement will show a GEOMETRYCOLLECTION.

image

The drawing part on top of Virtual Earth leverages a couple of events which can be attached to the Virtual Earth Map and I published the complete code on Via Virtual Earth. So I don’t want to go into detail for this part.

[I just noticed that this sample has been removed so now you’ll find it here].

The major difference between the code on “Via Virtual Earth” and the code in this sample application is that we create an additional checkbox with an onclick-event attached. Once we activate the checkbox we call a function to update the SQL statement in the textarea:

function SetInfo()
{
    myCurrentShape.SetTitle(document.getElementById("txtShapeTitle").value);
    myCurrentShape.SetDescription(document.getElementById("txtShapeDetails").value + "<br><a href='javascript:Delete("" + myCurrentShape.GetID() + "")'>Delete</a>");

    document.getElementById("divGeom").innerHTML = document.getElementById("divGeom").innerHTML + 
"<input id='" + myCurrentShape.GetID() + "' type='checkbox' onclick='UpdateSQLQuery(" +
myCurrentShape.GetID() + ")' />" + document.getElementById("txtShapeTitle").value + "<br>";
myPoints = new Array(); tempPoints = null; myDistance = 0; tempDistance = 0; document.getElementById("divShapeInfo").style.visibility = "hidden"; document.getElementById("txtShapeTitle").value = ""; document.getElementById("txtShapeDetails").value = ""; }

Updating the SQL-statement in the textarea requires first to determine the types of the VEShape-objects which have been activated.

function UpdateSQLQuery(id)
{
    var currentShape = slDrawing.GetShapeByID(id.id);
    switch (currentShape.GetType())
    {
        case "Point":
            pointCount++;
            var tempArray = currentShape.GetPoints();
            for(var j = 0; j < (tempArray.length); j++)
            {  
                pointArray.push(tempArray[j].Latitude + " " + tempArray[j].Longitude);
            }
            break;
        case "Polyline":
            polylineCount++;
            var tempArray = currentShape.GetPoints();
            var dummy = "";
            for(var j = 0; j < (tempArray.length); j++)
            {  
                dummy = dummy + tempArray[j].Latitude + " " + tempArray[j].Longitude + ", ";
            }            
            dummy = dummy.substr(0, dummy.length - 2);
            polylineArray.push(dummy);
            break;
        case "Polygon":
            polygonCount++;
            var tempArray = currentShape.GetPoints();
            var dummy = "";
            for(var j = 0; j < (tempArray.length); j++)
            {  
                dummy = dummy + tempArray[j].Latitude + " " + tempArray[j].Longitude + ", ";
            }            
            dummy = dummy.substr(0, dummy.length - 2);
            polygonArray.push(dummy);
            break;
    }

Now we determine which type of geography in SQL Server 2008 matches our selection.

if (pointCount == 1 && polylineCount == 0 && polygonCount == 0) geomType = "POINT";
else if (pointCount > 1 && polylineCount == 0 && polygonCount == 0) geomType = "MULTIPOINT";
else if (pointCount == 0 && polylineCount == 1 && polygonCount == 0) geomType = "LINESTRING";
else if (pointCount == 0 && polylineCount > 1 && polygonCount == 0) geomType = "MULTILINESTRING";
else if (pointCount == 0 && polylineCount == 0 && polygonCount == 1) geomType = "POLYGON";
else if (pointCount == 0 && polylineCount == 0 && polygonCount > 1) geomType = "MULTIPOLYGON";
else geomType = "GEOMETRYCOLLECTION";

Finally we create our SQL-statement.

    switch (geomType)
    {
        case "POINT":
            sqlBuilder = "INSERT INTO FeatureDemo (Geom) VALUES (geography::STGeomFromText('POINT (" + pointArray[0] + ")', 4326))";
            document.getElementById("txtSQL").value = sqlBuilder;
            break;
        case "MULTIPOINT":
            var dummy = "";
            for(var j = 0; j < (pointArray.length); j++)
            {  
                dummy = dummy + pointArray[j] + ", ";
            }            
            dummy = dummy.substr(0, dummy.length - 2);
            sqlBuilder = "INSERT INTO FeatureDemo (Geom) VALUES (geography::STGeomFromText('MULTIPOINT (" + dummy + ")', 4326))";
            document.getElementById("txtSQL").value = sqlBuilder;
            break;
        case "LINESTRING":
            sqlBuilder = "INSERT INTO FeatureDemo (Geom) VALUES (geography::STGeomFromText('LINESTRING (" + polylineArray[0] + ")', 4326))";
            document.getElementById("txtSQL").value = sqlBuilder;
            break;
        case "MULTILINESTRING":
            var dummy = "";
            for(var j = 0; j < (polylineArray.length); j++)
            {  
                dummy = dummy + "(" + polylineArray[j] + "), ";
            }            
            dummy = dummy.substr(0, dummy.length - 2);
            sqlBuilder = "INSERT INTO FeatureDemo (Geom) VALUES (geography::STGeomFromText('MULTILINESTRING (" + dummy + ")', 4326))";
            document.getElementById("txtSQL").value = sqlBuilder;
            break;
        case "POLYGON":
            sqlBuilder = "INSERT INTO FeatureDemo (Geom) VALUES (geography::STGeomFromText('POLYGON ((" + polygonArray[0] + "))', 4326))";
            document.getElementById("txtSQL").value = sqlBuilder;
            break;
        case "MULTIPOLYGON":
            var dummy = "";
            for(var j = 0; j < (polygonArray.length); j++)
            {  
                dummy = dummy + "((" + polygonArray[j] + ")), ";
            }            
            dummy = dummy.substr(0, dummy.length - 2);
            sqlBuilder = "INSERT INTO FeatureDemo (Geom) VALUES (geography::STGeomFromText('MULTIPOLYGON (" + dummy + ")', 4326))";
            document.getElementById("txtSQL").value = sqlBuilder;
            break;
        case "GEOMETRYCOLLECTION":
            var dummy = "";
            if (pointArray.length > 0)
            {
                for(var j = 0; j < (pointArray.length); j++)
                {  
                    dummy = dummy + "POINT(" + pointArray[j] + "), ";
                }            
            }
            if (polylineArray.length > 0)
            {
                for(var j = 0; j < (polylineArray.length); j++)
                {  
                    dummy = dummy + "LINESTRING(" + polylineArray[j] + "), ";
                }            
            }
            if (polygonArray.length > 0)
            {
                for(var j = 0; j < (polygonArray.length); j++)
                {  
                    dummy = dummy + "POLYGON((" + polygonArray[j] + ")), ";
                }            
            }
            dummy = dummy.substr(0, dummy.length - 2);
            sqlBuilder = "INSERT INTO FeatureDemo (Geom) VALUES (geography::STGeomFromText('GEOMETRYCOLLECTION (" + dummy + ")', 4326))";
            document.getElementById("txtSQL").value = sqlBuilder;
            break;
    }
}

Now that we have drawn our geographies and dynamically created our SQL-statement we only need to execute it. As in previous examples we will use an AJAX-call but unlike in the previous examples we will use an HTTP-Post request since the SQL-statements with all the coordinates can become quite long.

function SqlInsert()
{
    //Get the appropriate XMLHTTP object for the browser
    var xmlhttp = GetXmlHttp();
    
    //if we have a valid XMLHTTP object
    if (xmlhttp)
    {
        xmlhttp.open("POST", "./Insert.ashx", false);
        xmlhttp.setRequestHeader("Content-Type","application/x-www-form-urlencoded");
        //set the callback
        xmlhttp.onreadystatechange = function()
        {
            if (xmlhttp.readystate ==4) //4 is a success
            {
                //server code creates JavaScript "on the fly" for us to
                //execute using eval()
                var result = xmlhttp.responseText
                eval(result);
            }
        }
        xmlhttp.send("sql=" + document.getElementById("txtSQL").value);
    }
}

The HTTP-handler which is being called by the AJAX-call is even simpler than anything we had before. We just fetch the SQL-statement from the, set up the database connection and execute the statement. A message will be returned to the AJAX-call to indicate success or possible problems with the statement.

'Fetch URL-parameter
Dim mySQL1 As String = context.Request.Params("sql")

'Prepare database
Dim settings As ConnectionStringSettings = ConfigurationManager.ConnectionStrings("SpatialDB")

Dim myConn1 As New SqlConnection(settings.ConnectionString)
myConn1.Open()
Dim myCMD1 As New SqlCommand(mySQL1, myConn1)

Try
    myCMD1.ExecuteNonQuery()
    context.Response.Write("alert('SQL-Statement Executed');")
Catch ex As Exception
    context.Response.Write("alert(" + """" + ex.Message + """" + ");")
End Try

All right, we’re done. Run your page and test it.

image

You can also retrieve the results within SQL Server Management Studio…

SELECT Geom.STAsText() FROM FeatureDemo
---------------------------------------
POINT (51.461166621237787 -0.9271699190139665)
MULTIPOINT ((51.461149909867657 -0.9271699190139665), (51...))
POLYGON ((51.461380526235004 -0.92682123184203891, 51...))
MULTIPOLYGON (((51.461380526235004 -0...))((...)))

…and do further analysis like determining the size of the area which is covered by a polygon.

SELECT Geom.STArea() FROM FeatureDemo WHERE ID = 4
--------------------------------------------------
2532.6 square meter

Note: Polygons must be drawn counter clockwise and each polygon figure must be within a single, logical hemisphere. Individual spatial objects do not need to be within the same logical hemisphere in order to run spatial operations between them.

 

Advertisements
This entry was posted in Uncategorized. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s