{
let svg = htl.html`
<svg width="${canvasWidth}" height="${canvasHeight}">
<rect x="0" y="0" width="${canvasWidth}" height="${canvasHeight}" fill="#eee"></rect>
<g class="grid"></g>
<g class="points"></g>
<g class="vectors"></g>
<g class="estimates"></g>
</svg>
`;
let x = d3.scaleLinear(cameraExtent.x, [0, canvasWidth]);
let z = d3.scaleLinear(cameraExtent.z, [0, canvasHeight]);
let width = cameraExtent.x[1] - cameraExtent.x[0];
let gridSize = 1;
while (width / gridSize > 16) {
gridSize *= 2;
}
d3.select(svg)
.select(".grid")
.selectAll("line.vertical")
.data(
d3.range(
roundDownTo(cameraExtent.x[0], gridSize),
roundUpTo(cameraExtent.x[1], gridSize),
gridSize
)
)
.join("line")
.classed("vertical", true)
.attr("x1", (d) => x(d))
.attr("y1", 0)
.attr("x2", (d) => x(d))
.attr("y2", canvasHeight)
.attr("stroke", "#aaa");
// horizontal grid lines
d3.select(svg)
.select(".grid")
.selectAll("line.horizontal")
.data(
d3.range(
roundDownTo(cameraExtent.z[0], gridSize),
roundUpTo(cameraExtent.z[1], gridSize),
gridSize
)
)
.join("line")
.classed("horizontal", true)
.attr("x1", 0)
.attr("y1", (d) => z(d))
.attr("x2", canvasWidth)
.attr("y2", (d) => z(d))
.attr("stroke", "#aaa");
// measurement points
d3.select(svg)
.select(".points")
.selectAll("circle")
.data(measurements)
.join("circle")
.attr("cx", (d) => x(d.x))
.attr("cy", (d) => z(d.z))
.attr("r", 5)
.attr("fill", "black");
// measurement angles
const vectorLength = 30;
d3.select(svg)
.select(".vectors")
.selectAll("line")
.data(measurements)
.join("line")
.attr("x1", (d) => x(d.x))
.attr("y1", (d) => z(d.z))
.attr("x2", (d) => x(d.x) + Math.cos(d.angRad) * 30)
.attr("y2", (d) => z(d.z) + Math.sin(d.angRad) * 30)
.attr("stroke", "black");
// measurement labels
d3.select(svg)
.select(".points")
.selectAll("text")
.data(measurements)
.join("text")
.attr("x", (d) => x(d.x))
.attr("y", (d) => z(d.z))
.attr("dy", -12)
.text((d) => `x=${d.x?.toFixed()} z=${d.z?.toFixed()}`)
.attr("text-anchor", "end")
.attr("font-size", 12)
.attr("fill", "#444");
// intersection
d3.select(svg)
.select(".estimates")
.selectAll("circle.intersection")
.data(intersections)
.join("circle")
.classed("intersection", true)
.attr("cx", (d) => x(d.x))
.attr("cy", (d) => z(d.z))
.attr("r", 1)
.attr("fill", "black");
d3.select(svg)
.select(".estimates")
.selectAll("circle.error")
.data([intersection])
.join("circle")
.classed("error", true)
.attr("cx", (d) => x(d.x))
.attr("cy", (d) => z(d.z))
.attr("r", (d) => d.radius)
.attr("stroke", "rgba(0, 0, 255, 0.4)")
.attr("lineWidth", 2)
.attr("fill", "none");
d3.select(svg)
.select(".estimates")
.selectAll("text")
.data([intersection, intersection])
.join("text")
.attr("x", (d) => x(d.x))
.attr("y", (d) => z(d.z))
.attr("dy", (d, i) => d.radius + (i + 1) * 15)
.attr("text-anchor", "end")
.text((d, i) =>
i % 2 == 0 ? `x=${d.x?.toFixed()}` : `z=${d.z?.toFixed()}`
);
return svg;
}