Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 50 additions & 12 deletions app/plugin/LineSliceHighlight.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,60 @@ Ext.define('CpsiMapview.plugin.LineSliceHighlight', {
*/
style: null,

constructor: function (config) {
this.callParent([config]);
if (config && config.style) {
this.style = config.style;
}
},

init: function () {
this.style = new ol.style.Style({
stroke: new ol.style.Stroke({
color: 'red',
width: 2
})
});
if (!this.style) {
this.style = new ol.style.Style({
stroke: new ol.style.Stroke({
color: 'red',
width: 2
})
});
}
},

/**
* Measure a segment in the units the chainages are expressed in.
*
* @param {ol.geom.LineString} segment
* @param {ol.proj.Projection} projection
* @returns {number} length in metres
*/
getSegmentLength: function (segment, projection) {
if (projection && projection.getUnits() === 'degrees') {
return ol.sphere.getLength(segment, {
projection: projection.getCode()
});
}
if (projection && projection.getCode() === 'EPSG:3857') {
// Web Mercator grid distances are inflated by 1/cos(latitude),
// so planar length is not usable here
return ol.sphere.getLength(segment, { projection: 'EPSG:3857' });
}
return segment.getLength();
},

/**
* Calculates the slice of geometry from start to end
* @param {ol.geom.LineString} geometry
* @param {number} start in m
* @param {number} end in m
* @param {ol.proj.Projection} projection projection of the geometry
* @returns {ol.geom.LineString}
*/
calculateSlice: function (geometry, start, end) {
calculateSlice: function (geometry, start, end, projection) {
const me = this;
let length = 0;
const coordinates = [];
geometry.forEachSegment(function (a, b) {
const segment = new ol.geom.LineString([a, b]);
const segmentLength = ol.sphere.getLength(segment);
const segmentLength = me.getSegmentLength(segment, projection);
if (length <= start && start < length + segmentLength) {
// start is in this segment
coordinates.push(
Expand All @@ -44,7 +76,7 @@ Ext.define('CpsiMapview.plugin.LineSliceHighlight', {
length + segmentLength <= end
) {
// the endpoint of the segment is between start and end
// openlayers forEachSegment reuses the arrays for the coordinates so it needs to be cloned
// OpenLayers forEachSegment reuses the arrays for the coordinates so it needs to be cloned
coordinates.push(b.slice());
}
if (length <= end && end < length + segmentLength) {
Expand All @@ -64,9 +96,15 @@ Ext.define('CpsiMapview.plugin.LineSliceHighlight', {
* @param {ol.geom.LineString} geometry
* @param {number} start
* @param {number} end
* @param {ol.Map} [map] defaults to the application's main map
*/
highlightSlice: function (geometry, start, end) {
const map = BasiGX.util.Map.getMapComponent().map;
highlightSlice: function (geometry, start, end, map) {
if (!map) {
map = BasiGX.util.Map.getMapComponent().map;
}

const projection = map.getView().getProjection();

if (!this.layer) {
this.layer = new ol.layer.Vector({
style: this.style,
Expand All @@ -78,7 +116,7 @@ Ext.define('CpsiMapview.plugin.LineSliceHighlight', {
}

const feature = new ol.Feature(
this.calculateSlice(geometry, start, end)
this.calculateSlice(geometry, start, end, projection)
);

this.layer.getSource().addFeature(feature);
Expand Down
140 changes: 140 additions & 0 deletions test/spec/plugin/LineSliceHighlight.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
describe('CpsiMapview.plugin.LineSliceHighlight', function () {
Ext.Loader.syncRequire(['CpsiMapview.plugin.LineSliceHighlight']);

// a straight 1000 m East-West line in Irish Transverse Mercator
const makeLine = function () {
return new ol.geom.LineString([
[714000, 729000],
[714500, 729000],
[715000, 729000]
]);
};

const itm = new ol.proj.Projection({
code: 'EPSG:2157',
units: 'm'
});

const makePlugin = function () {
const plugin = new CpsiMapview.plugin.LineSliceHighlight();
plugin.init();
return plugin;
};

describe('Basics', function () {
it('is defined', function () {
expect(CpsiMapview.plugin.LineSliceHighlight).not.to.be(undefined);
});

it('can be created', function () {
const plugin = new CpsiMapview.plugin.LineSliceHighlight();
expect(plugin).to.not.be(undefined);
});

it('sets a default style on init', function () {
const plugin = makePlugin();
expect(plugin.style).to.not.be(null);
});

it('does not overwrite a configured style', function () {
const style = new ol.style.Style({
stroke: new ol.style.Stroke({ color: 'blue', width: 9 })
});
const plugin = new CpsiMapview.plugin.LineSliceHighlight({
style: style
});
plugin.init();
expect(plugin.style).to.be(style);
});

it('can set a style', function () {
const plugin = makePlugin();
const style = new ol.style.Style({
stroke: new ol.style.Stroke({ color: 'green', width: 1 })
});
plugin.setStyle(style);
expect(plugin.style).to.be(style);
});
});

describe('calculateSlice', function () {
it('measures projected geometry as planar length', function () {
const plugin = makePlugin();
const slice = plugin.calculateSlice(makeLine(), 0, 1000, itm);
expect(Math.round(slice.getLength())).to.be(1000);
});

it('slices from the start of the line', function () {
const plugin = makePlugin();
const slice = plugin.calculateSlice(makeLine(), 0, 250, itm);
expect(Math.round(slice.getLength())).to.be(250);
});

it('slices across a vertex', function () {
const plugin = makePlugin();
const slice = plugin.calculateSlice(makeLine(), 400, 600, itm);
const coords = slice.getCoordinates();
expect(Math.round(slice.getLength())).to.be(200);
expect(Math.round(coords[0][0])).to.be(714400);
expect(Math.round(coords[coords.length - 1][0])).to.be(714600);
});

it('slices to the end of the line', function () {
const plugin = makePlugin();
const slice = plugin.calculateSlice(makeLine(), 750, 1000, itm);
expect(Math.round(slice.getLength())).to.be(250);
});

it('keeps the intermediate vertex in a full slice', function () {
const plugin = makePlugin();
const slice = plugin.calculateSlice(makeLine(), 0, 1000, itm);
expect(slice.getCoordinates().length).to.be(3);
});

it('does not mutate the source geometry', function () {
const plugin = makePlugin();
const line = makeLine();
const before = JSON.stringify(line.getCoordinates());
plugin.calculateSlice(line, 100, 900, itm);
expect(JSON.stringify(line.getCoordinates())).to.be(before);
});

it('handles a north-south line', function () {
const plugin = makePlugin();
const line = new ol.geom.LineString([
[714000, 729000],
[714000, 730000]
]);
const slice = plugin.calculateSlice(line, 0, 1000, itm);
expect(Math.round(slice.getLength())).to.be(1000);
});

it('handles a diagonal line', function () {
const plugin = makePlugin();
const line = new ol.geom.LineString([
[714000, 729000],
[714300, 729400]
]);
// hypotenuse is exactly 500 m
const slice = plugin.calculateSlice(line, 0, 500, itm);
expect(Math.round(slice.getLength())).to.be(500);
});
});

describe('removeHighlight', function () {
it('does nothing when there is no layer', function () {
const plugin = makePlugin();
plugin.removeHighlight();
expect(plugin.layer).to.be(undefined);
});

it('clears the layer reference', function () {
const plugin = makePlugin();
plugin.layer = new ol.layer.Vector({
source: new ol.source.Vector()
});
plugin.removeHighlight();
expect(plugin.layer).to.be(null);
});
});
});
Loading