Azure Maps - Snapping to Shapes When Using the Drawing Tools

Ken Bowman 231 Reputation points
2024-10-09T15:57:22.6333333+00:00

Hi,

I'm trying to replicate a feature that I have in my Bing Maps application using Azure Maps. I have created a distance measuring tool using the Azure Map's DrawingManager to draw a circle and show the distance using a Popup.

User's image

It works great, but I need to be able to snap the starting position to a potential pushpin shape located where initially clicked on the map, and as I move over pushpins, snap to their location so that I can produce an accurate distance between locations (e.g. if the mouse moves over a pushpin it jumps to the anchor point and stays there until the mouse moves out of the pushpin).

Can this be accomplished using Azure Maps?

Thanks,

Ken

Azure Maps
Azure Maps
An Azure service that provides geospatial APIs to add maps, spatial analytics, and mobility solutions to apps.
723 questions
0 comments No comments
{count} votes

3 answers

Sort by: Most helpful
  1. IoTGirl 3,126 Reputation points Microsoft Employee
    2024-10-09T16:51:45.05+00:00

    Hi Ken,

    We have tried to replicate all Bing Maps for Enterprise Samples at Azuremaps.samples.com. Can you take a look at https://samples.azuremaps.com/drawing-tools-module/create-a-measuring-tool and see if that works for you? The source code is available in github.

    Sincerely,

    IoTGirl


  2. Ken Bowman 231 Reputation points
    2024-10-10T16:22:20.31+00:00

    Hi,

    I've made a GIF to try to show what I need to do using Azure Maps (the GIF is based on my Bing Maps app). Unfortunately, the cursor isn't visible, however, the tooltip gives a clue where the mouse is as I drag.

    In the GIF, I initially clicked on the center pushpin and the cursor snapped to the tip of it. Then as I drag the cursor, when the cursor goes over another pushpin the circle snaps to the tip of that pushpin and stays there until the mouse leaves that pushpin. This allows me to display an accurate distance from one pushpin to another.

    Thanks,

    Ken

    SnapToPushpinAnimation

    0 comments No comments

  3. rbrundritt 18,266 Reputation points Microsoft Employee
    2024-10-10T19:10:22.6466667+00:00

    The drawing tools does have a snapping feature, however it is grid based (you define a grid resolution, and all drawn shapes will snap to that).

    For your scenario, it sounds like you want to snap to another shapes positions rather than a grid. It's a similar idea, but how to achieve this would depend on the type of shapes you are working with and how you want to snap the shapes together (points are fairly easy, while lines and polygons can get complicated fast).

    It sounds like you are primarily working with points. The way I would handle this scenario is as follows:

    1. Add a mouse move event to map and monitor what the mouse is over and keep track of what shape the mouse is over top of (or no shape at all). Mouse in/out doesn't work well on layers since moving between two overlapping points in the same layer wouldn't fire either of these events since it is a layer level event, thus why using the maps mouse move event is being used.
    2. When drawing the shape for measuring, wait for the drawing changed event to fire. This event is fired whenever a point is added to a shape. For lines and polygons this means each vertices fires the event, with the exception of the first point (if you want to snap that too, also watch the drawing started event when not in edit mode). For circles and rectangles, the drawing changed event should only fire when you release the mouse, and the shape has been drawn. IF you also want to support editing the drawn measurement, things get a bit more complicated as the changed event fires constantly as the movement of any point means the shape of the original shape has changed (there is no preview of a change when in edit mode). The key thing for circles is that you would need to calculate the snapped radius to update the circle. Similarly, for rectangles you would need to recalculate the corners (take first point and snap/hovered point, and simply use all lat/lon combos to create a rectangle).

    The tricky part comes with lines and polygons as the preview line that appears also needs to be updated and there is no documented way to do this. Basically after updating our shapes coordinates we need to ensure the last coordinate in the is in sync with the preview line data. This is a workaround, but here's how it's done:

    drawingManager.drawingHelper.activeShapeCoordinates = shape.getCoordinates();
    

    If you are using typescript you may need to add //@ts-ignore above this line of code as I suspect there might be an IntelliSense error.

    So, assuming you don't need the option to edit the measurement shape, this scenario isn't overly difficult to achieve. Below I have taken the measuring tool code sample and modified it to support snapping to points that are already on the map.

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <title></title>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
    
        <!-- Add references to the Azure Maps Map control JavaScript and CSS files. -->
        <link href="https://atlas.microsoft.com/sdk/javascript/mapcontrol/3/atlas.min.css" rel="stylesheet" />
        <script src="https://atlas.microsoft.com/sdk/javascript/mapcontrol/3/atlas.min.js"></script>
    
        <!-- Add references to the Azure Maps Map Drawing Tools JavaScript and CSS files. -->
        <link rel="stylesheet" href="https://atlas.microsoft.com/sdk/javascript/drawing/1/atlas-drawing.min.css" type="text/css" />
        <script src="https://atlas.microsoft.com/sdk/javascript/drawing/1/atlas-drawing.min.js"></script>
    
        <script>
            var map, datasource, drawingManager, mainLayer;
            var hoveredPoint;
            var hoveredStartPoint;
    
            function getMap() {
                //Initialize a map instance.
                map = new atlas.Map('myMap', {
                    center: [-90, 35],
                    zoom: 3,
                    view: 'Auto',
    
                    //Add authentication details for connecting to Azure Maps.
                    authOptions: {
                        authType: 'subscriptionKey',
                        subscriptionKey: '<Your Azure Maps Key>'
                    }
                });
    
                //Wait until the map resources are ready.
                map.events.add('ready', function () {
                    //Add a datasource and layer of points that we want to allow snapping to.
                    datasource = new atlas.source.DataSource();
                    map.sources.add(datasource);
    
                    //Add some data to the data source. Using USGS earthquake data for a simple example.
                    datasource.importDataFromUrl('https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_month.geojson');
    
                    //Add a layer for rendering the points.
                    mainLayer = new atlas.layer.SymbolLayer(datasource, null, {
                        iconOptions: {
                            allowOverlap: true
                        }
                    });
                    map.layers.add(mainLayer);
    
                    //Monitor the mouse movement over the map and see which, if any, symbols the mouse is overtop of.
                    map.events.add('mousemove', mouseMoved);
    
                    //Create an instance of the drawing manager and display the drawing toolbar.
                    drawingManager = new atlas.drawing.DrawingManager(map, {
                        toolbar: new atlas.control.DrawingToolbar({
                            buttons: ['draw-line', 'draw-polygon', 'draw-rectangle', 'draw-circle'],
                            position: 'top-right',
                            style: 'light'
                        })
                    });
    
                    //Clear the map and drawing canvas when the user enters into a drawing mode.
                    map.events.add('drawingmodechanged', drawingManager, drawingModeChanged);
    
                    //Monitor for when a polygon drawing has been completed.
                    map.events.add('drawingstarted', drawingManager, drawingStarted);
                    map.events.add('drawingchanging', drawingManager, measureShape);    //Fires when in shape preview is shown.
                    map.events.add('drawingchanged', drawingManager, drawingChanged);     //Fires when the shape has changed.
                    map.events.add('drawingcomplete', drawingManager, drawingComplete); //Fires when drawing is completed.
                });
            }
    
            //When the mouse moves, figure out which shape the mouse is over.
            function mouseMoved(e) {
                hoveredPoint = null;
    
                //Check to see if the map is over top of any shape.
                if (document.getElementById('snapPoints').checked && e.shapes) {
    
                    //Loop through shapes to see if any of them are from the main layer.
                    //Need to do this as the shape we are drawing can end up under the mouse and be the top shape, so we need to look under it.
                    for (let i = 0; i < e.shapes.length; i++) {
                        const s = e.shapes[i];
    
                        //Determine if the hovered shape is from the main layer.
                        //Unless our data is in a vector tile source, or our data is clusterd, our data should be atlas.Shape objects.
                        //The base map data is in vector tiles and are GeoJson features, so we can skip those.
                        //We will assuming clustering is not enabled for this example.
                        if (s instanceof atlas.Shape) {
                            //Now get the id of the hovered shape and see if it is in our data source that is connected to the layer.
                            //If it is, then we know it is one of the shapes we want to snap to.
                            //If you have multiple layers / sources in you want to allow snapping to, simply loop through them here.
                            if (datasource.getShapeById(s.getId())) {
                                //This if statement can be combined with the above, seperated here for clarity.
                                hoveredPoint = s.getCoordinates();
    
                                //We can break out of the loop now.
                                break;
                            }
                        }
                    }
                }
            }
    
            function drawingStarted() {
                //We can't edit the shapes points at this stage, so will have to do this later. Capture the hovered start point now.
                hoveredStartPoint = null;
    
                //Check to see if the user wants to allow the snapping the initial drawn point. 
                if (document.getElementById('snapFirstPoint').checked && hoveredPoint) {
                    //In some scenarios you may want to be able to measure from any arbitrary point on the map and in others you may want to snap to the hovered point.
                    hoveredStartPoint = hoveredPoint;
                }
            }
    
            function drawingModeChanged(mode) {
                //Clear the drawing canvas when the user enters into a drawing mode.
                if (mode.startsWith('draw')) {
                    drawingManager.getSource().clear();
                    document.getElementById('measurementInfo').innerHTML = '';
                }
            }
    
            function drawingComplete(shape) {
                //Exit drawing mode.
                drawingManager.setOptions({ mode: 'idle' });
    
                measureShape(shape);
            }
    
            function drawingChanged(shape) {
                //Update the shape with the snapped point(s).
                snapShape(shape);
    
                //Measure the shape.
                measureShape(shape);
            }
    
            function snapShape(shape) {
                //Check to see if any point in our main layer is hovered over. If not, no need to do any snapping.
                if (hoveredPoint || hoveredStartPoint) {
                    const drawingMode = drawingManager.getOptions().mode;
    
                    //Get the snapping options.
                    const snapFirstPoint = document.getElementById('snapFirstPoint').checked;
                    const snapLastPoint = document.getElementById('snapPoints').checked;
                    const allowCollapsedShapes = document.getElementById('allowCollapsedShapes').checked;
    
                    //Get the coordinates of the shape.
                    let coords = shape.getCoordinates();
    
                    if (coords.length > 0) {
                        //If the user is hovering over the same shape when starting and ending a drawing, it could potentially collapse the drawn shape, leading to zero area.
                        //Collapsed shapes can result in the map/drawing manager throwing an error in some cases.
                        //Here we add the option to allow and handle this.
                        if (!allowCollapsedShapes) {
                            //Check to see if hoveredStartPoint and hoveredPoint are the same.
                            //If so, disable one of them otherwise we risk the shape collapsing.
                            if (hoveredStartPoint && hoveredPoint && hoveredStartPoint[0] === hoveredPoint[0] && hoveredStartPoint[1] === hoveredPoint[1]) {
                                //Can allow if drawing line and have 3+ points, or a polygon and have 4+ points as there will be atleast one other unique point.
                                if (!(drawingMode === 'draw-line' && coords.length >= 3) &&
                                    !(drawingMode === 'draw-polygon' && coords[0].length >= 4)) {
                                    //Add custom logic on how you want to handle this. For now keeping it simple and disabling snapping of first point.
                                    //Disabling snapping to the first point as that likely makes the most sense.
                                    hoveredStartPoint = null;
                                }
                            }
                        }
    
                        if (drawingMode === 'draw-circle') {
                            //Snap the center point if the user wants to.
                            if (snapFirstPoint && hoveredStartPoint) {
                                coords = hoveredStartPoint;
    
                                //Update the center coordinate.
                                shape.setCoordinates(coords);
                            }
    
                            //Snap the radius if the user wants to.
                            if (snapLastPoint && hoveredPoint) {
                                //Calculate the distance from the center to the hovered point.
                                const distance = atlas.math.getDistanceTo(coords, hoveredPoint, 'meters');
    
                                //Update the radius property of the circle.
                                const props = shape.getProperties();
                                props.radius = distance;
                                shape.setProperties(props);
                            }
                        } else {
                            if (drawingMode === 'draw-rectangle') {
                                let firstPoint = coords[0][0];
    
                                //A rectangle is a polygon, so coords[0] is the first outer ring.
                                //For rectangles the third coordinate (index: 2), is the opposite corner.
                                let oppositePoint = coords[0][2];
    
                                //Snap the first point if the user wants to.
                                if (snapFirstPoint && hoveredStartPoint) {
                                    firstPoint = hoveredStartPoint;
                                }
    
                                //Snap the last point of the opposite corner if the user wants to.
                                if (snapLastPoint && hoveredPoint) {
                                    oppositePoint = hoveredPoint;
                                }
    
                                //Recalculate the corners of the rectangle.  Since rectangle is a polygon, have to update the first ring.
                                coords[0] = [
                                    firstPoint,
                                    [firstPoint[0], oppositePoint[1]],
                                    oppositePoint,
                                    [oppositePoint[0], firstPoint[1]],
                                    firstPoint //Have to close the polygon ring as best practice.
                                ];
    
                                shape.setCoordinates(coords);
                            } else if (drawingMode === 'draw-line' || drawingMode === 'draw-polygon') {
                                //Line and polygon drawing are handles in the same way.
    
                                //Snap the first point if the user wants to.
                                if (snapLastPoint && hoveredStartPoint) {
                                    coords[0] = hoveredStartPoint;
                                }
    
                                //Snap the last point if the user wants to.
                                if (snapLastPoint && hoveredPoint) {
                                    coords[coords.length - 1] = hoveredPoint;
                                }
    
                                shape.setCoordinates(coords);
    
                                //WORKAROUND: Need to update active shape coordinates info in drawing helper to ensure preview line and all further re-renders of the line are updated
                                drawingManager.drawingHelper.activeShapeCoordinates = coords;
                            }
                        }
                    }
                }
            }
    
            function measureShape(shape) {
                var msg = '';
    
                //If the search area is a circle, create a polygon from its circle coordinates.
                if (shape.isCircle()) {
                    var r = atlas.math.convertDistance(shape.getProperties().radius, 'meters', 'miles', 2);
                    var a = Math.round(2 * Math.PI * r * r * 100) / 100;
                    var p = Math.round(2 * Math.PI * r * 100) / 100;
    
                    msg = `Radius: ${r} mi<br/>Area: ${a} sq mi<br/>Perimeter: ${p} mi`;
                } else {
                    var g = shape.toJson().geometry;
                    var polygon;
    
                    switch (shape.getType()) {
                        case 'LineString':
                            var l = Math.round(atlas.math.getLengthOfPath(g.coordinates, 'miles') * 100) / 100;
                            msg += `Length: ${l} mi`;
    
                            //Polygon's are rendered as lines when initially being drawn.
                            if (drawingManager.getOptions().mode === 'draw-polygon') {
                                polygon = new atlas.data.Polygon(g.coordinates);
                            }
                            break;
                        case 'Polygon':
                            polygon = g;
    
                            var p = Math.round(atlas.math.getLengthOfPath(g.coordinates[0], 'miles') * 100) / 100;
                            msg = `Perimeter: ${p} mi`;
                            break;
                    }
    
                    if (polygon) {
                        msg += `<br/>Area: ${atlas.math.getArea(polygon, 'squareMiles', 2)} sq mi`;
                    }
                }
    
                document.getElementById('measurementInfo').innerHTML = msg;
            }
        </script>
    </head>
    <body onload="getMap()">
        <div id="myMap" style="position:relative;width:100%;min-width:290px;height:600px;"></div>
    
        <!-- Toggle for snapping mode, and measurement info display. -->
        <div style="position: absolute; top: 10px; left: 10px; background-color: white; border-radius: 10px; padding: 10px;">
            Snap points: <input type="checkbox" id="snapPoints" checked="checked" />
            <br />
            Snap first point: <input type="checkbox" id="snapFirstPoint" checked="checked" />
            <br />
            Allow collapsed shapes: <input type="checkbox" id="allowCollapsedShapes" checked="checked" />
            <br />
            <br />
            <div id="measurementInfo"></div>
        </div>
    </body>
    </html>
    

    I've made a live version of the above code available here: https://rbrundritt.azurewebsites.net/Demos/AzureMaps/MeasureSnapToPoint.html

    I'm assuming you aren't drawing points since they would have a measurement of 0. But if you wanted to draw a point and have it snap to a hovered existing point, monitor the drawing complete event instead of the drawing changed event when in point drawing mode. There are no change events for points.


Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.