clickable polyline/polygon

We can use pushin to create a clickable point to display information, what about clickable polylines and polygons For example, there is a street closed due to pipe line break, or new construction cause 2 miles of interstate highway closed, etc. I want to be able to do similar things with those lines, for example, hover or click on the line, and get information popup window, like those push pins.
I know you can not attach events to a line for now, what about future versions
I want to know if this can be done in client side, not capture the clicked point and send an AJAX request to a server performing spatial query on that location.

Thanks a lot,


Answer this question

clickable polyline/polygon

  • Dlloyd

    Gabor:
    Thanks. That was helpful. This solution is exactly what Caleb T proposed above, with the downside of having to loop through all your graphics collections to check if it is within anyone of them. Although the algorithm itself is pretty efficient, it can be quite a headache when you have mixed lines/points/polygons in your map--and lots of them, and you want to do mouseover or mouse move (thus much  more compuation needed).

    The solution I am suggesting above, is natively attech events to the underline graphic element itself, so you can have more events, faster, and you can attach your events when you creating the object. SVG and VML objects all listen to different mouse events. For example, for vml:
    <v:shape id="ploygon1"
    fillcolor="green" style="top:1;left:1;width:650;height:600"
    path = "m 350,75 l 379,161 469,161 397,215 423,301 350,250 277,301 303,215 231,161 321,161 350,75 x e" onclick="showStuff(this)" onmouseover="fancystuff()"> </v:shape>

    for svg:
    <div id="svgDiv">
    <svg xmlns="http://www.w3.org/2000/svg" >
    <g style="stroke:green;">
    <polygon id="polygon1" fill="red" stroke-width="10" fill-opacity="0.0"
    points="350,75 379,161 469,161 397,215
    423,301 350,250 277,301 303,215
    231,161 321,161" onclick="showStuff(this)" />
    </g>
    </svg>
    </div>

    The suggestion SoulSolutions proposed above,i.e., finding the actual element and manually attach events to it, provides a new angle of view. Unfortunately, unlike pushpins that uses an "<A" tag with id same as pushpin id, the id of the actual graphic element (v:shape, svg:line etc is not same as the VEPolyline/Polygon id. and even worse, VE delete/creates them everytime when map view is changed, and assign a different id from an auto increamental number, which means there is no linkage what-so-ever between a pushpin/polyline/polygon id and the underline graphics primitive element id. As a result, the getElementById(polylineId) etc won't do the trick, and even you assigned the event to the element manually, the event is gone because the orginal element is gone everytime you pan/zoom.

    Anyway, keep digging...



  • Sankar Anand

    cocgis, you seem like you know what is needed here.

    When you create a div you can assign an attribute to it like this:

    var el = document.createElement("div");

    el.setAttribute('id',"VELabel" + LabelID++);

    Can you do something similar with the shapes If so this could be a temporary way to do it

    Clearly we don't actually get the object but we could loop through them once on creation to find the new one (the one without the id set)

    John.



  • JoshNewbury

    Yup, that's MUCH better, and that's what I'd like to do. But since the creation of the polyline or shape is buried inside of the VE code, it's hard to make that happen. I looked into overriding the VE logic a bit, but it's pretty hard to read (compressed, variable names are short, no comments, etc.)

    I also made this suggestion to VE support, but haven't heard back about that. One possible simple, but cheesy, implementation would be to add a method like VEPolygon.setUnderlyingObjectItem(key, value). As a client, you could tell VEPolygon to set something on the underlying object, as you indicated. For example, a call like:

    var poly = new VEPolygon(poly_id, points);
    poly.setUnderlyingObjectItem('parent_poly', poly);

    would let us get the VEPolygon from the polyline or shape.

    Even simpler, I suppose, would be if the VEPolygon just did this without exposing a new call- that is, if VEPolygon just added parent_poly to any polyline or shape it creates (with value 'self'.)

    It's cheesy, but it'd work pretty well.

  • Selva Kumar.T.P

    This is the place to let the team know what they would need to do to help you and others.

    I assume what would be ideal is to be able to specify a onclick event handler as part of the:

    var x = new VEPolygon(id, locations, fillColor, outlineColor, outlineWidth);

    or better still to use the same approach as for VEPushPin's and have a Property to set the callback function.

    If you look at how current do an onclick for a pushpin

    var pin = new VEPushpin(pinID, location, icon_url, title, details, iconStyle);

    map.AddPushpin(pin);

    var element = document.getElementById(pinID);

    element.onclick = EventHandlerOnClick;

    Maybe there is a simple way to get to that underling VML or SVG element and add the eventhandler manually I'm guessing it is more tricky as its a set of lines and you would need to abstract IE and firefox

    John.



  • Satyajit

    We can easily take this the next step. In your polygon click event handler you used the event to get the coordinates, however, there's some very interesting properties of the event that we're not taking advantage of. For instance, the event has a srcElement property which is the VML shape element that the click originated from. Among it's attributes is the 'id' of the shape, which is going to be in the 50000 range. If you dig around in the VE javascript mess you'll see that they set starting points for the IDs of polylines, polygons and other stuff, polygons start at 50000. Important to note, however, is that the ID of your first polygon isn't 50000, that number is actually being incremented twice for each polygon added, the whole range appears to be reset for each addition, and your polygon IDs are the second half. For instance, if you've added 5 polygons, the ID's of your 5 polygons will be 50005, 50006, 50007, 50008, 50009. I haven't tracked down exactly what the first half (50000-50004) is being used for. So, if you are maintaining an array of your polygons(in the order that they were added) then from you click event handler you can determine which polygon was clicked.

    In my example I'm using the click to zoom down to a particular polygon. 'polygons' is the array of polygons that I've added.

    function onPolyClick()

    {

    map.DeleteAllPolygons();

    map.AddPolygon(polygons[(event.srcElement.id)-(50000+polygons.length)]);

    map.SetMapView(polygons[(event.srcElement.id)-(50000+polygons.length)].LatLongs);

    }

    Steven


  • hazz

    Common guys let us know how you made your polygons and polylines clickable.
    John.


  • Mazeno

    This is very cool, but I wanted to handle onMouseover as well.  Doing onMouseover for the whole map seemed problematic- I didn't want to have a ton of JavaScript run every time the mouse moved.

    I got around this by following cocgis' suggestion- attach handlers to the underlying polyline or shape elements.  It's not production-ready, I think, but here's what I did (hope it helps someone.)

    When adding the VEPolygons, I record them into a global array:

           map.AddPolygon(poly);
           polys.push(poly);

    After adding all the relevant VEPolygons, my code runs through the graphics elements, adding a handler for each, like this:

       //MES- Run through the polylines (for Firefox), adding handlers to each
       var polylines = document.getElementsByTagName('polyline');
       for (var iPoly = 0; iPoly < polylines.length; ++iPoly)
       {
        var poly = polylines[iPoly];
        poly.onmouseover = onMouseoverPoly;
        poly.onclick = onClickPoly;
       }

       //MES- Run through the shapes (for IE), adding handlers to each
       var shapes = document.getElementsByTagName('shape');
       for (var iShape = 0; iShape < shapes.length; ++iShape)
       {
        var shape = shapes[iShape];
        shape.onmouseover = onMouseoverPoly;
        shape.onclick = onClickPoly;
       }

    The onClick handler looks like this:

     function onClickPoly()
     {
      var ll = latLongFromEvt(event);
      var poly = findPoly(ll);
      if (null != poly)
      {
       // Do something useful with the poly
      }
     }

    The latLongFromEvt helper looks like this:

     function latLongFromEvt(e)
     {
      //MES- Use cached numbers for mapLeft and mapTop- it seems like
      // in IE, if we call map.GetLeft() or map.GetTop() more than
      // once, subsequent calls return 0.
      var mapXPixel = e.clientX - mapLeft;
      var mapYPixel = e.clientY - mapTop;
      return map.PixelToLatLong(mapXPixel,mapYPixel);
     }

    and the findPoly helper looks like this:

     function findPoly(latlong)
     {
      for (var i = 0; i < polys.length; ++i)
      {
       var poly = polys[ i ];
       if (poly.inZone(latlong))
       {
        return poly;
       }
      }

      return null;
     }

    It makes use of the poly.inZone function that you wrote (thanks!)

    The onMouseover handler was a little trickier because of rounding errors.  Using the naive approach (like onClickPoly) didn't work very well- a good fraction of the time, the polygon wouldn't be found.  I think this was a rounding error- if the mouse is right on the border of the polygon, converting the screen coordinates to lat/long would result in a lat/long that wasn't in the polygon.  To get around this, I just try a few points near the point like this (anybody have any better suggestions   This is kinda crappy):

     function onMouseoverPoly()
     {
      var ll = latLongFromEvt(event);
      //MES- Look for a polygon for the lat/long
      var poly = findPoly(ll);
      var fudge_lat = 0.0003;
      var fudge_long = 0.0003;

      //MES- Did not find a polygon, we are probably SLIGHTLY off the polygon (a
      // rounding error.)  Try slightly above the point.
      if (null == poly)
      {
       var llUp = new VELatLong(ll.Latitude + fudge_lat, ll.Longitude);
       poly = findPoly(llUp);
      }

      //MES- Above did not work, try below
      if (null == poly)
      {
       var llDown = new VELatLong(ll.Latitude - fudge_lat, ll.Longitude);
       poly = findPoly(llDown);
      }

      //MES- Below did not work, try left
      if (null == poly)
      {
       var llLeft = new VELatLong(ll.Latitude, ll.Longitude - fudge_long);
       poly = findPoly(llLeft);
      }

      //MES- left did not work, try right
      if (null == poly)
      {
       var llRight = new VELatLong(ll.Latitude, ll.Longitude + fudge_long);
       poly = findPoly(llRight);
      }

      if (null != poly)
      {
       // Do something useful with the poly
      }
     }


  • Chuck Turner

    I was able to create clickable polgons by modifying some JavaScript code using the Google Earth API written for this purpose by Jim Miller. It required using some undocumented features of VEPolygon, which I uncovered by inspecting the VEPolygon object in the debugger.

    Hope this helps,

    Gabor

    <html>

    <head>

    <title></title>

    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">

    <script src="http://dev.virtualearth.net/mapcontrol/v3/mapcontrol.js"></script>

    <script>

    var map = null;

    var pinID = 1;

    function GetMap()

    {

    map = new VEMap('myMap');

    map.LoadMap();

    AddPolygon();

    }

    function AddPolygon()

    {

    var points = new Array(

    new VELatLong(38.52024,-121.54175),

    new VELatLong(38.3998,-121.54175),

    new VELatLong(38.3998,-121.28082),

    new VELatLong(38.52024,-121.28082),

    new VELatLong(38.42024,-121.4),

    new VELatLong(38.52024,-121.54175)

    );

    poly = new VEPolygon('polygon1',points);

    poly.SetOutlineWidth(3);

    poly.SetOutlineColor(new VEColor(0,150,100,1.0));

    poly.SetFillColor(new VEColor(0,150,100,0.5));

    //alert(poly.points[0].Longitude);

    map.AddPolygon(poly);

    map.SetMapView(points);

    //map.AttachEvent('onclick', myEventTester);

    map.AttachEvent('onmouseup', myEventTester);

    }

    function myEventTester(e)

    {

    if (poly.inZone(e.view.LatLong))

    alert('You clicked INSIDE the polygon');

    }

    // Determines if a point is inside a polygon

    VEPolygon.prototype.inZone = function(pt) {

    var isInZone = 0;

    var j = this.LatLongs.length - 1;

    for (var k = 0; k < this.LatLongs.length; j = k++) {

    if ((((this.LatLongs[k].Latitude <= pt.Latitude) && (pt.Latitude < this.LatLongs[j].Latitude)) || ((this.LatLongs[j].Latitude <= pt.Latitude) && (pt.Latitude < this.LatLongs[k].Latitude))) && (pt.Longitude < (this.LatLongs[j].Longitude - this.LatLongs[k].Longitude) * (pt.Latitude - this.LatLongs[k].Latitude) / (this.LatLongs[j].Latitude - this.LatLongs[k].Latitude) + this.LatLongs[k].Longitude))

    {

    isInZone++;

    }

    }

    if ((isInZone % 2) == 0)

    return false;

    else

    return true;

    }

    </script>

    </head>

    <body onload="GetMap();">

    <div id='myMap' style="position: relative; width: 400px; height: 400px;">

    </div>

    </body>

    </html>


  • Aleks

    Not to discouraging your effort but trying to use inZone sort of approach defeats the whole purpose of event attaching.

    The whole idea of event attaching, is that you will garantee to get the exact polyline/polygon you clicked/mouseovered/whatever from the browser. You should never have to run function like point in poly, or point near line sort of function if you are using events. You can at least use the srcElement (as pointed out by Steven later) to identify itself as the object been clicked/moveovered). In the case of the very thick polyline or boundary line of a polygon, the browser will adjust it's event trapping based on the rendering the the graphics (SVG or VML shape) and still fire up the correct events, while any sort of caculation based on coordinates will likely to be incorrect or affected by the linethickness and offset tolenrance(set for point-line relationship). Trying to convert between lat/lng and pixel for these type of purpose does not seem to be on the right path.

    VE creates a new set of graphics elements everytime map view is changed. There is a method called "CreatePrimitive" for both SVG and VML. This is the right place to attach events if VE team considers events enabled graphics is a nice feature. To attach events to the DOM element after it is created, it is necessary to reattach all events everytime map view changed.

    Your method of holding a global polygon array and then use getElementsByTagName("polygon") and trying to match the polygon in your array to the actual DOM element is a nice try, but unfortuanately won't work. The index in the array will only match in a very limited scenarios. As pointed out above, VE wipes off all graphices and recreate everyone anytime map view changed. Say, you created 5 polygon, the size of the poly array is 5, you pan your map, one is outside view port, only 4 is visible. The getElementsByTagName will return 4, not 5. Depending on which poly in the original array is invisible now, it is impossible to rematch the elements and VEpolys, unless you recreate ALL VEpolys EVERYTIME the graphic elements are recreated such as map view change. (Why VE decided to wipe everything off and recreate them so frequently I have no idea).

    Now, those 50000, 30000, 40000 numbers. Since VE wipe the plate clean so often, it calls a function called GetGeoUID to increments the number and use the result to assign id to the grapics element. (If you use FireFox's DOM inspector, you can see those 3000XX, 5000XX numbers ). However, I feel wat VE really wants is to assign the VEPolyID, which is supposed to be unique at least for all VEPolygons, to the really graphic element ID. (a.setAttribute("id",d.id!=0 d.id:d.iid) in the CreatePrimitive method). But the really VEpolyid is never passed in, so the "iid", which is the result of GetGeoUID is always been used. if you play more, you will find a simple bug inside the Polyline iid (not Polygon). You will always get same 40000 for all polylines. The VE programmer mistakenly treated a function as a property in the prototype.

    So really, there are two ways to deal with this issue nicely without using any of these point-poly, point-line type of functions, which are resource intensive and subject to error due to the rendering of the graphics(line thickness, pixel tolenrance etc):

    1. Attach events during the graphics elements creation. That basically require those event handlers saved in some sort of event table for the VEPoly objects, then attach to the DOM elements in the DrawPrimitive function when they are been created.

    or:
    2.Attach events after the primitives are created. Attached events to the primitive elements by finding out the correct elements for the correct VEPoly etc. I would create a consistant way to match the ids between them. For example, if I created a VEPoly with id 12345, I will always create it's SVG polygon or VML shape with id "VE_POLYGON_12345".

    Both these require some changes (not very significant I think) in the VE lib, which may running into terms issue. I am trying to have lawyers review it's term in more detail before spending any more time on it. Or the VE team can just do it, right if they think it's a feature that a lot of people wants and allow them have an new edge over Google Map, right

    Please, if you think this is something usefull, let the VE team know: we want "event aware graphics"! Hopefully, they will put that in their to-do list.









  • ano

    mmm you are so very close.

    Unfortunatly I've had zero time to spend on this. I like the fact you can assign an event to the polygons in both IE and Firefox. What would be better is if you could identify the polygon at the point of creation. Is it possible to set a custom attribute to the polygon object I know this is possible with other js objects.

    If it is possible you could create a new attribute called 'VEPolygonID' and set the ID from VE as the reference to do whatever needs to happen later. Then when you create a new polygon you could simply loop through all the polygons until you find one that doesn't have the attribute set Maybe a little slower on creation but then when the event fires you instantly have the ID. Still faster then looping every polygon on every event.

    John.



  • jhurliman

    Thanks.If the map area has a lot of lines, using brutal force approach to find whether a point is on every single line seems slow, and building some sort of spatial index seems too much work for javascript.

    <br/>While I was wondering, I saw the collections scrach pad in VE. After you draw a line, then you use the pick tool, you can SELECT the line to edit it! Isn't that a clickable line already The question is: Is it possible to use that "selection" code to click the line, then maybe create an invisible push pin to display information about the line Since those are undocumented function, I sort of hoping someone with more knowledge about VE to give me some insights before diving into it. Many thanks again!

  • ZZgun

    Can someone suggest a good JavaScript prettyprinter I've been trying to override the normal behavior, but I'm having a hard time modifying the source code in its "raw" form (i.e. compacted.)

  • GunaChinna

    Did some extra research on this topic.
    Well, it turns out, there is really no need to do any sort of point-polygon calculation to implement this.
    In Virtual Earth, the graphics are implemented with VML (for IE) or SVG (for Firefox etc). Fortunately, both VML and SVG's graphics elements response to mouse events (did not check all events, but at least onclick, onmouseover events), so it is really just a matter of attaching some handlers to the target element (e.g. <v:line or <svg:polyline> etc), then display data or ajax something. The browser will make sure your mouse is on that element previsely. However, that may require some code revision to the actual VE javascripts lib instead of using the exposed API,and may raise licensing concerns.

  • gifuran

    Well, if you know the area that is "hot" (ie, where you have a region you want clickable), you could track the mouse movement, convert the position to a latlong, and then look up that latlong to see if it is in your "hot" region. There's a lot of logic that needs to be in there to determine whether a given point lies within a polygon (or over a line), especialy when said polygon is actually on a globe. But I know some--if not all--of that logic has been published. Some online searching for "point in polygon" will yield helpful results.

    And as you state, we don't have clickable lines/polygons in the VE control at this point. I don't dare make predictions about what the control will have...


  • clickable polyline/polygon