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>