How to make lines appear as curves in a simple LineLayer instead of straight lines in Azure Maps?

Arjun Ramesh 20 Reputation points
2023-04-25T15:19:33.66+00:00

We have a requirement to plot some points on Azure Indoor Maps and connect the points using lines to show path. For this we are using LineLayer (to draw the line) and two SymbolLayer (one for plotting the points and another for showing direction arrows along the path). The problem we are facing is that we are not able to show any curves. When joining from point A to Point B to Point C, it is showing as straight lines. Is there any options to change this linear behavior and show the path between 2 points as a free-flowing curve? I have tried using lineCap and lineJoin as "round", but the behavior was the same.

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

Accepted answer
  1. rbrundritt 20,921 Reputation points Microsoft Employee Moderator
    2023-04-26T22:22:20.95+00:00

    Assuming you don't just want to round the line joins and caps, curved lines can be drawn but require calculating the points that approximate the curve. Note that you will loop through your data before adding it to the data source, calculate the curved versions of your lines, then add those to the data source. The atlas.math namespace has two methods that can be used to inflate a line and create curves; atlas.math.getCardinalSpline docs, atlas.math.getGeodesicPath docs and atlas.math.getGeodesicPaths docs.

    Alternatively, you can also use a custom algorithm to calculate a curve. For example, here is a class for calculating cubic Bezier curves:

    class CubicBezier {
    
    	/**
    	* Takes an array of positions can calculates a cubic bezier curve through it. Returns a MultiLineString (a curve per position pair).
    	* @param positions An array of positions that form a path.
    	* @param tension A number that indicates the tightness of the curve. Can be any number, although a value between 0 and 1 is usually used. Default: 0.5
    	* @param nodeSize The number of nodes to insert between each position. Default: 15			
    	*/
    	static getLine(positions, tension = 0.5, nodeSize = 15) {
    		var segments = [];
    		
    		for(var i = 0; i < positions.length - 1; i++){			
    			segments.push(this.getCurvedSegments(positions[i], positions[i + 1], tension, nodeSize));
    		}
    		
    		return new atlas.data.MultiLineString(segments);
    	}
    	
    	/**
    	* Calculates a cubic bezier curve between two points.
    	* @param p1 First position.
    	* @param p2 Second position.
    	* @param tension A number that indicates the tightness of the curve. Can be any number, although a value between 0 and 1 is usually used. Default: 0.5
    	* @param nodeSize The number of nodes to insert between each position. Default: 15
    	*/
    	static getCurvedSegments(p1, p2, tension = 0.5, nodeSize = 15){
    		var len = atlas.math.getDistanceTo(p1, p2);
    		var heading = atlas.math.getHeading(p1, p2);
    		
    		var h1, h2;
    		
    		if (heading < 0) {
    			h1 = heading + 45;
    			h2 = heading + 135;
    		} else {
    			h1 = heading - 45;
    			h2 = heading - 135;
    		}
    
    		var pA = atlas.math.getDestination(p1, h1, len * tension);
    		var pB = atlas.math.getDestination(p2, h2, len * tension);
    
    		return this.#calculateCubicBezier(p1, pA, pB, p2, nodeSize);
    	}
    	
    	static #calculateCubicBezier(p1, p2, p3, p4, nodeSize){
    		var path = [];
    
    		for (var i = 0; i < nodeSize; i++) {
    			path.push(this.#getBezier(p1, p2, p3, p4, i/nodeSize));
    		}
    		
    		path.push(p1);
    		
    		return path;
    	}
    
    	static #getBezier(p1, p2, p3, p4, t) {
    		var b1 = t * t * t;
    		var b2 = 3 * t * t * (1 - t);
    		var b3 = 3 * t * (1 - t) * (1 - t);
    		var b4 = (1 - t) * (1 - t) * (1 - t);
    	
    		return [
    			p1[0] * b1 + p2[0] * b2 + p3[0] * b3 + p4[0] * b4,
    			p1[1] * b1 + p2[1] * b2 + p3[1] * b3 + p4[1] * b4
    		];
    	};		
    }
    
    //Usage
    var positions = [ /* positions of your original path*/ ];
    var line = CubicBezier.getLine(positions);
    

    Now, since you are doing indoor paths, you likely want to avoid the path going through walls and only really want a curve where two lines join with some radius. This would be a bit more defined than the lineJoin option which simply rounds the edges of the line based on the lines width. The following is an example function for calculating this:

    /**
    * Takes an array of positions and rounds the corners between line segments.
    * @param positions An array of positions that form a path.
    * @param cornerOffset How far away from a corner in meters the curve should start. Default: 5
    * @param nodeSize The number of nodes to insert between each position. Default: 15		
    */
    function RoundedCorners(positions, cornerOffset = 5, nodeSize = 15){
    
    	//Add the start position of the line.
    	var path = [positions[0]];
    	
    	var lineEnd;
    	
    	for(var i = 1; i < positions.length; i++){
    
    		//Get start and points of line segment.
    		var p1 = positions[i - 1];
    		var p2 = positions[i];		
    		
    		//Calculate the length and heading of the line.
    		var h = atlas.math.getHeading(p1, p2);					
    		var len = atlas.math.getDistanceTo(p1, p2);
    		
    		//Ensure the corner offset is no longer than half the length of the line, otherwise we will have some knots in the line.
    		var offset = Math.min(len * 0.5, cornerOffset);
    		
    		//Calculate the offset first point of the linesegment. This is also the end of the previous line segment.
    		var lineStart = atlas.math.getDestination(p1, h, offset);
    		
    		//Calculate curve from last end of previous line segment line.
    		if(lineEnd){
    			//Calculate the midpoint between the previous lines offset end and the current lines offset start.
    			var midPoint = atlas.math.interpolate(lineEnd, lineStart, 0.5);
    			
    			//Calculate the distance and heading from the offset paths, midpoint to the corner coordinate.
    			var midToP1Distance = atlas.math.getDistanceTo(midPoint, p1);
    			var midToP1Heading = atlas.math.getHeading(midPoint, p1);
    			
    			//Calculate the corner coordinate. 
    			var corner = atlas.math.getDestination(midPoint, midToP1Heading, midToP1Distance * 0.75);
    
    			//Calculate a spline curve around the corner.
    			var curve = atlas.math.getCardinalSpline([lineEnd,corner, lineStart]);
    			
    			//Add the curve to the path.
    			path = path.concat(curve);
    		}
    		
    		//Add the start of the new line segment to the path.
    		path.push(lineStart);
    		
    		//Calculate the end of the line segment and add it to the path.
    		lineEnd = atlas.math.getDestination(p2, h, -offset);
    		path.push(lineEnd);
    	}
    	
    	//Add the end position of the line.
    	path.push(positions[positions.length - 1]);
    	
    	return path;
    }
    
    //Usage
    var positions = [ /* positions of your original path*/ ];
    var line = new atlas.data.LineString(RoundedCorners(positions));
    

    The geodesic paths calculate a curve based on the curvature of the earth. For indoor maps the distance between two points in a path may be too short for the curve to be that noticeable, so a cardinal spline, cubic Bezier might be better suited. Here is a full example that compares these three methods:

    <!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 rel="stylesheet" href="https://atlas.microsoft.com/sdk/javascript/mapcontrol/2/atlas.min.css" type="text/css" />
        <script src="https://atlas.microsoft.com/sdk/javascript/mapcontrol/2/atlas.min.js"></script>
    
        <script>
            var map, datasource;
    		
    		var originalLine = {
    			type: 'Feature',
    			geometry: {
    				type: 'LineString',
    				coordinates:  [
    					[-122.203812,47.61456],
    					[-122.203831,47.615602],
    					[-122.203945,47.615593],
    					[-122.20397,47.616131],
    					[-122.204831,47.616122],
    					[-122.204882,47.616763],
    					[-122.205356,47.616754],
    					[-122.205394,47.617304],
    					[-122.203482,47.617317],
    					[-122.203457,47.616942],
    					[-122.203318,47.616805],
    					[-122.203115,47.616178],
    					[-122.20183,47.616203]
    				]
    			},
    			properties: {
    				pathType: 'original'
    			}
    		};
    
            function GetMap() {
                //Initialize a map instance.
                map = new atlas.Map('myMap', {
                    center: [-122.204396, 47.615568],
                    zoom: 17,
                    view: 'Auto',
    
                    authOptions: {
                        authType: 'subscriptionKey',
                        subscriptionKey: '<Your Azure Maps Key>'
                    }
                });
    
                //Wait until the map resources are ready.
                map.events.add('ready', function () {
    
                    //Create a data source and add it to the map.
                    datasource = new atlas.source.DataSource();
                    map.sources.add(datasource);
    
                    //Create a layer for visualizing the lines on the map.
                    map.layers.add(new atlas.layer.LineLayer(datasource, null, {
                        strokeWidth: [
    						'match',
    						['get', 'pathType'],
    						'original', 10,
    						4
    					],
                        strokeColor: [
    						'match',
    						['get', 'pathType'],
    						'original', 'Black',
    						'cardinalSpline', 'DodgerBlue',
    						'cubicBezier' , 'DeepPink',
    						'roundedCorners', 'LightSeaGreen',
    						'black'					
    					]
                    }));
    								
    				recalculateCurves();
                });
            }
    		
    		function recalculateCurves() {
    			var tension = parseFloat(document.getElementById('Tension').value);
    			var nodeSize = parseInt(document.getElementById('Nodesize').value);
    			
    			var lines = [originalLine];
    						
    			lines.push(new atlas.data.Feature(
    				new atlas.data.LineString(RoundedCorners(originalLine.geometry.coordinates, nodeSize)), {
    					pathType: 'roundedCorners' 
    				})
    			);
    			
    			lines.push(new atlas.data.Feature(
    				new atlas.data.LineString(
    					atlas.math.getCardinalSpline(originalLine.geometry.coordinates, tension, nodeSize)
    				), {
    					pathType: 'cardinalSpline' 
    				})
    			);
    
    			lines.push(new atlas.data.Feature(
    				CubicBezier.getLine(originalLine.geometry.coordinates, tension, nodeSize), {
    					pathType: 'cubicBezier' 
    				})
    			);
    			
    			datasource.setShapes(lines);
    		}
    		
    		class CubicBezier {
    
    			/**
    			* Takes an array of positions can calculates a cubic bezier curve through it. Returns a MultiLineString (a curve per position pair).
    			* @param positions An array of positions that form a path.
    			* @param tension A number that indicates the tightness of the curve. Can be any number, although a value between 0 and 1 is usually used. Default: 0.5
    			* @param nodeSize The number of nodes to insert between each position. Default: 15			
    			*/
    			static getLine(positions, tension = 0.5, nodeSize = 15) {
    				var segments = [];
    				
    				for(var i = 0; i < positions.length - 1; i++){			
    					segments.push(this.getCurvedSegment(positions[i], positions[i + 1], tension, nodeSize));
    				}
    				
    				return new atlas.data.MultiLineString(segments);
    			}
    			
    			/**
    			* Calculates a cubic bezier curve between two points.
    			* @param p1 First position.
    			* @param p2 Second position.
    			* @param tension A number that indicates the tightness of the curve. Can be any number, although a value between 0 and 1 is usually used. Default: 0.5
    			* @param nodeSize The number of nodes to insert between each position. Default: 15
    			*/
    			static getCurvedSegment(p1, p2, tension = 0.5, nodeSize = 15){
    				var len = atlas.math.getDistanceTo(p1, p2);
    				var heading = atlas.math.getHeading(p1, p2);
    				
    				var h1, h2;
    				
    				if (heading < 0) {
    					h1 = heading + 45;
    					h2 = heading + 135;
    				} else {
    					h1 = heading - 45;
    					h2 = heading - 135;
    				}
    
    				var pA = atlas.math.getDestination(p1, h1, len * tension);
    				var pB = atlas.math.getDestination(p2, h2, len * tension);
    
    				return this.#calculateCubicBezier(p1, pA, pB, p2, nodeSize);
    			}
    			
    			static #calculateCubicBezier(p1, p2, p3, p4, nodeSize){
    				var path = [];
    
    				for (var i = 0; i < nodeSize; i++) {
    					path.push(this.#getBezier(p1, p2, p3, p4, i/nodeSize));
    				}
    				
    				path.push(p1);
    				
    				path.reverse();
    				
    				return path;
    			}
    
    			static #getBezier(p1, p2, p3, p4, t) {
    				var b1 = t * t * t;
    				var b2 = 3 * t * t * (1 - t);
    				var b3 = 3 * t * (1 - t) * (1 - t);
    				var b4 = (1 - t) * (1 - t) * (1 - t);
    			
    				return [
    					p1[0] * b1 + p2[0] * b2 + p3[0] * b3 + p4[0] * b4,
    					p1[1] * b1 + p2[1] * b2 + p3[1] * b3 + p4[1] * b4
    				];
    			};		
    		}
    		
    		/**
    		* Takes an array of positions and rounds the corners between line segments.
    		* @param positions An array of positions that form a path.
    		* @param cornerOffset How far away from a corner in meters the curve should start. Default: 5
    		* @param nodeSize The number of nodes to insert between each position. Default: 15		
    		*/
    		function RoundedCorners(positions, cornerOffset = 5, nodeSize = 15){
    			
    			//Add the start position of the line.
    			var path = [positions[0]];
    			
    			var lineEnd;
    			
    			for(var i = 1; i < positions.length; i++){
    
    				//Get start and points of line segment.
    				var p1 = positions[i - 1];
    				var p2 = positions[i];		
    				
    				//Calculate the length and heading of the line.
    				var h = atlas.math.getHeading(p1, p2);					
    				var len = atlas.math.getDistanceTo(p1, p2);
    				
    				//Ensure the corner offset is no longer than half the length of the line, otherwise we will have some knots in the line.
    				var offset = Math.min(len * 0.5, cornerOffset);
    				
    				//Calculate the offset first point of the linesegment. This is also the end of the previous line segment.
    				var lineStart = atlas.math.getDestination(p1, h, offset);
    				
    				//Calculate curve from last end of previous line segment line.
    				if(lineEnd){
    					//Calculate the midpoint between the previous lines offset end and the current lines offset start.
    					var midPoint = atlas.math.interpolate(lineEnd, lineStart, 0.5);
    					
    					//Calculate the distance and heading from the offset paths, midpoint to the corner coordinate.
    					var midToP1Distance = atlas.math.getDistanceTo(midPoint, p1);
    					var midToP1Heading = atlas.math.getHeading(midPoint, p1);
    					
    					//Calculate the corner coordinate. 
    					var corner = atlas.math.getDestination(midPoint, midToP1Heading, midToP1Distance * 0.75);
    
    					//Calculate a spline curve around the corner.
    					var curve = atlas.math.getCardinalSpline([lineEnd,corner, lineStart]);
    					
    					//Add the curve to the path.
    					path = path.concat(curve);
    				}
    				
    				//Add the start of the new line segment to the path.
    				path.push(lineStart);
    				
    				//Calculate the end of the line segment and add it to the path.
    				lineEnd = atlas.math.getDestination(p2, h, -offset);
    				path.push(lineEnd);
    			}
    			
    			//Add the end position of the line.
    			path.push(positions[positions.length - 1]);
    			
    			return path;
    		}
        </script>
    	<style>
    	html, body {
    		width: 100%;
    		height: 100%;
    		margin: 0;
    		padding: 0;
    	}
    	
    	 #myMap {
    		position: relative;
    		width: 100%;
    		height: 100%;
    	}
    	
    	.panel {
    		position:absolute;
    		top:10px;
    		left:10px;
    		padding:10px;
    		background-color:white;
    		border-radius:10px;
    	}
    	</style>
    </head>
    <body onload="GetMap()">
        <div id="myMap"></div>
    	
    	<div class="panel">
    		 <table>
    			<tr>
    				<td>Tension:</td>
    				<td>
    					<form oninput="tension.value=Tension.value">
    						<input type="range" id="Tension" value="0.5" min="-1" max="1" step="0.05" oninput="recalculateCurves()" onchange="recalculateCurves()" />
    						<output name="tension" for="Tension">0.5</output>
    					</form>
    				</td>
    			</tr>
    			<tr>
    				<td>Node size:</td>
    				<td>
    					<form oninput="nodeSize.value=Nodesize.value">
    						<input type="range" id="Nodesize" value="15" min="2" max="100" step="1" oninput="recalculateCurves()" onchange="recalculateCurves()" />
    						<output name="nodeSize" for="Nodesize">15</output>
    					</form>
    				</td>
    			</tr>
    			<tr>
    				<td><svg viewBox="0 0 20 5" xmlns="http://www.w3.org/2000/svg"><line x1="5" y1="2" x2="15" y2="2" stroke="Black" /></svg></td>
    				<td>Original line</td>
    			</tr>
    			<tr>
    				<td><svg viewBox="0 0 20 5" xmlns="http://www.w3.org/2000/svg"><line x1="5" y1="2" x2="15" y2="2" stroke="DodgerBlue" /></svg></td>
    				<td>Cardinal Spline</td>
    			</tr>
    			<tr>
    				<td><svg viewBox="0 0 20 5" xmlns="http://www.w3.org/2000/svg"><line x1="5" y1="2" x2="15" y2="2" stroke="DeepPink" /></svg></td>
    				<td>Cubic Bezier</td>
    			</tr>
    			<tr>
    				<td><svg viewBox="0 0 20 5" xmlns="http://www.w3.org/2000/svg"><line x1="5" y1="2" x2="15" y2="2" stroke="LightSeaGreen" /></svg></td>
    				<td>Rounded corners</td>
    			</tr>
    		</table>
    	</div>
    </body>
    </html>
    
    2 people found this answer helpful.

0 additional answers

Sort by: Most helpful

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.