Skip to content

Commit 385cf05

Browse files
authored
Merge pull request #56 from henrythasler/feature/splitCircle
Feature/split circle
2 parents 92e96cc + 8299a7e commit 385cf05

11 files changed

Lines changed: 275 additions & 104 deletions

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ Leaflet.Geodesic is available from [unpkg](https://unpkg.com/browse/leaflet.geod
2222

2323
Add it in your nodejs-project with `npm i leaflet.geodesic`.
2424

25+
If possible, pin the plug-in to a specific version and use [Subresource Integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity). Check the [release page](https://github.com/henrythasler/Leaflet.Geodesic/releases) for the latest version, links and checksum. A checksum can by verified with `npm run build`, is stored in `dist/leaflet.geodesic.umd.min.js.sha256` on [jsDelivr](https://www.jsdelivr.com/package/npm/leaflet.geodesic?path=dist) and [unpkg](https://unpkg.com/browse/leaflet.geodesic@2.5.1/dist/leaflet.geodesic.umd.min.js.sha256) and is shown in the [build-log](https://travis-ci.org/henrythasler/Leaflet.Geodesic/builds) for a tagged version.
26+
2527
## Basic usage
2628

2729
- `L.Geodesic` draws geodesic lines between all points of a given line- or multiline-string.
@@ -164,8 +166,8 @@ const Berlin = new L.LatLng(52.5, 13.35);
164166
const LosAngeles = new L.LatLng(33.82, -118.38);
165167
const Beijing = new L.LatLng(39.92, 116.39);
166168

167-
const geodesic = new L.Geodesic([Berlin, LosAngeles]).addTo(map); // add empty object to the map
168-
geodesic.addLatLng(Beijing)
169+
const geodesic = new L.Geodesic([Berlin, LosAngeles]).addTo(map);
170+
geodesic.addLatLng(Beijing); // results in [[Berlin, LosAngeles, Beijing]
169171
```
170172

171173
The new point will always be added to the last linestring of a multiline. You can define a specific linestring to add to by reading the `points` property before and hand over a specific linestring as second parameter:
@@ -227,7 +229,7 @@ The geometry of a circle can be updated with the following methods:
227229
- `setLatLng(latlng: L.LatLngExpression)` - set a new center
228230
- `setRadius(radius: number)` - update the radius
229231

230-
Handling of circles crossing the antimeridian (wrapping) is not yet supported.
232+
Handling of **filled** circles crossing the antimeridian (wrapping) is not yet supported. Set `fill: false` in these cases to avoid display artefacts.
231233

232234
### Circle Options
233235

docs/circle-interactive.html

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@
5353

5454
var map = L.map('map', {
5555
maxBounds: [
56-
[-90, -180],
57-
[90, 180]
56+
[-90, -200],
57+
[90, 200]
5858
],
5959
center: [50, 0],
6060
zoom: 4
@@ -90,16 +90,16 @@
9090

9191
// method that we will use to update the control based on feature properties passed
9292
info.update = function (stats) {
93-
const circumfenceString = (stats.totalDistance ? (stats.totalDistance>10000)?(stats.totalDistance/1000).toFixed(0)+' km':(stats.totalDistance).toFixed(0)+' m' : 'invalid')
94-
const radiusString = (geodesiccircle.radius ? (geodesiccircle.radius>10000)?(geodesiccircle.radius/1000).toFixed(0)+' km':(geodesiccircle.radius).toFixed(0)+' m' : 'invalid')
95-
this._div.innerHTML = '<h4>Statistics</h4><b>Radius</b><br/>' + radiusString +
96-
'<br/><br/><b>Circumfence</b><br/>' + circumfenceString +
93+
const circumfenceString = (stats.totalDistance ? (stats.totalDistance > 10000) ? (stats.totalDistance / 1000).toFixed(0) + ' km' : (stats.totalDistance).toFixed(0) + ' m' : 'invalid')
94+
const radiusString = (geodesiccircle.radius ? (geodesiccircle.radius > 10000) ? (geodesiccircle.radius / 1000).toFixed(0) + ' km' : (geodesiccircle.radius).toFixed(0) + ' m' : 'invalid')
95+
this._div.innerHTML = '<h4>Statistics</h4><b>Radius</b><br/>' + radiusString +
96+
'<br/><br/><b>Circumfence</b><br/>' + circumfenceString +
9797
'<br/><br/><b>Vertices</b><br/>' + stats.vertices;
98-
};
98+
};
9999

100100
info.update(geodesiccircle.statistics);
101101

102-
var diff = {lat:0, lng:0};
102+
var diff = { lat: 0, lng: 0 };
103103
A.on('dragstart', function () {
104104
diff = { lat: A.getLatLng().lat - B.getLatLng().lat, lng: A.getLatLng().lng - B.getLatLng().lng }; // remember difference between A and B
105105
});
@@ -110,8 +110,9 @@
110110
lat: Math.max(-90, Math.min(90, A.getLatLng().lat - diff.lat)),
111111
lng: A.getLatLng().lng - diff.lng
112112
}); // move B parallel to A
113-
geodesiccircle.setLatLng(e.latlng);
114-
geodesiccircle.setRadius(geodesiccircle.distanceTo(B.getLatLng()));
113+
114+
const radius = geodesiccircle.geom.distance(e.latlng, B.getLatLng());
115+
geodesiccircle.setLatLng(e.latlng, radius);
115116
info.update(geodesiccircle.statistics);
116117
});
117118
B.on('drag', (e) => {

docs/testing-circle.html

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@
5353

5454
var map = L.map('map', {
5555
maxBounds: [
56-
[-90, -180],
57-
[90, 180]
56+
[-90, -200],
57+
[90, 200]
5858
],
5959
center: [50, 0],
6060
zoom: 4
@@ -76,11 +76,12 @@
7676
opacity: 0.5,
7777
color: 'red',
7878
fill: true,
79-
steps: 80
79+
steps: 45,
80+
wrap: true
8081
}).bindTooltip("<Area>").bindPopup("GeodesicCircle").addTo(map);
8182

8283
geodesiccircle.setRadius(geodesiccircle.distanceTo(B.getLatLng()));
83-
geodesiccircle.setTooltipContent(`Area: ${Math.round(geodesiccircle.radius**2*Math.PI/1000**2)} km²`);
84+
geodesiccircle.setTooltipContent(`Area: ${Math.round(geodesiccircle.radius ** 2 * Math.PI / 1000 ** 2)} km²`);
8485

8586
var info = L.control();
8687
info.onAdd = function (map) {
@@ -91,16 +92,16 @@
9192

9293
// method that we will use to update the control based on feature properties passed
9394
info.update = function (stats) {
94-
const circumfenceString = (stats.totalDistance ? (stats.totalDistance>10000)?(stats.totalDistance/1000).toFixed(0)+' km':(stats.totalDistance).toFixed(0)+' m' : 'invalid')
95-
const radiusString = (geodesiccircle.radius ? (geodesiccircle.radius>10000)?(geodesiccircle.radius/1000).toFixed(0)+' km':(geodesiccircle.radius).toFixed(0)+' m' : 'invalid')
96-
this._div.innerHTML = '<h4>Statistics</h4><b>Radius</b><br/>' + radiusString +
97-
'<br/><br/><b>Circumfence</b><br/>' + circumfenceString +
95+
const circumfenceString = (stats.totalDistance ? (stats.totalDistance > 10000) ? (stats.totalDistance / 1000).toFixed(0) + ' km' : (stats.totalDistance).toFixed(0) + ' m' : 'invalid')
96+
const radiusString = (geodesiccircle.radius ? (geodesiccircle.radius > 10000) ? (geodesiccircle.radius / 1000).toFixed(0) + ' km' : (geodesiccircle.radius).toFixed(0) + ' m' : 'invalid')
97+
this._div.innerHTML = '<h4>Statistics</h4><b>Radius</b><br/>' + radiusString +
98+
'<br/><br/><b>Circumfence</b><br/>' + circumfenceString +
9899
'<br/><br/><b>Vertices</b><br/>' + stats.vertices;
99-
};
100+
};
100101

101102
info.update(geodesiccircle.statistics);
102103

103-
var diff = {lat:0, lng:0};
104+
var diff = { lat: 0, lng: 0 };
104105
A.on('dragstart', function () {
105106
diff = { lat: A.getLatLng().lat - B.getLatLng().lat, lng: A.getLatLng().lng - B.getLatLng().lng }; // remember difference between A and B
106107
});
@@ -111,16 +112,31 @@
111112
lat: Math.max(-90, Math.min(90, A.getLatLng().lat - diff.lat)),
112113
lng: A.getLatLng().lng - diff.lng
113114
}); // move B parallel to A
114-
geodesiccircle.setLatLng(e.latlng);
115-
geodesiccircle.setRadius(geodesiccircle.distanceTo(B.getLatLng()));
115+
116+
const radius = geodesiccircle.geom.distance(e.latlng, B.getLatLng());
117+
geodesiccircle.setLatLng(e.latlng, radius);
116118
info.update(geodesiccircle.statistics);
117-
geodesiccircle.setTooltipContent(`Area: ${Math.round(geodesiccircle.radius**2*Math.PI/1000**2)} km²`);
119+
120+
// console.log(geodesiccircle.getLatLngs().length);
121+
122+
geodesiccircle.setStyle({
123+
color: (geodesiccircle.getLatLngs().length === 1)?'red':'blue'
124+
})
125+
126+
// this is only valid for small radii where the resulting circle is flat compared to the earth surface
127+
geodesiccircle.setTooltipContent(`Area: ${Math.round(geodesiccircle.radius ** 2 * Math.PI / 1000 ** 2)} km²`);
118128
});
119129
B.on('drag', (e) => {
120130
B.setTooltipContent(`${Math.round(10000 * e.latlng.lat) / 10000} ${Math.round(10000 * e.latlng.lng) / 10000}`);
121131
geodesiccircle.setRadius(geodesiccircle.distanceTo(e.latlng));
122132
info.update(geodesiccircle.statistics);
123-
geodesiccircle.setTooltipContent(`Area: ${Math.round(geodesiccircle.radius**2*Math.PI/1000**2)} km²`);
133+
134+
geodesiccircle.setStyle({
135+
color: (geodesiccircle.getLatLngs().length === 1)?'red':'blue'
136+
})
137+
138+
// this is only valid for small radii where the resulting circle is flat compared to the earth surface
139+
geodesiccircle.setTooltipContent(`Area: ${Math.round(geodesiccircle.radius ** 2 * Math.PI / 1000 ** 2)} km²`);
124140
});
125141

126142
</script>

docs/testing.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,9 @@
106106
});
107107
B.on('drag', (e) => {
108108
// console.log(`B=${e.latlng}`);
109-
B.setTooltipContent(`${Math.round(10000 * e.latlng.lat) / 10000} ${Math.round(10000 * e.latlng.lng) / 10000}`);
109+
// B.setTooltipContent(`${Math.round(10000 * e.latlng.lat) / 10000} ${Math.round(10000 * e.latlng.lng) / 10000}`);
110+
B.setTooltipContent(`${map.latLngToLayerPoint(e.latlng)}`);
111+
110112
geodesic.setLatLngs([A.getLatLng(), e.latlng])
111113
info.update(geodesic.statistics);
112114
});

spec/geodesic-circle.test.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ describe("Main functionality", function () {
5050
it("Create class with parameters", function () {
5151
const circle = new GeodesicCircleClass(Beijing, { steps: 48 });
5252
expect(circle).to.be.instanceOf(GeodesicCircleClass);
53-
compareObject(circle.options, {...defaultOptions, ...{steps: 48}});
53+
compareObject(circle.options, { ...defaultOptions, ...{ steps: 48 } });
5454
});
5555

5656
it("Add empty circle to map", async function () {
@@ -72,6 +72,18 @@ describe("Main functionality", function () {
7272
expect(circle.radius).to.be.closeTo(radius, eps);
7373
});
7474

75+
it("update center with radius", async function () {
76+
const circle = new GeodesicCircleClass(Seattle).addTo(map);
77+
expect(circle).to.be.instanceOf(GeodesicCircleClass);
78+
compareObject(circle.options, defaultOptions);
79+
expect(map.hasLayer(circle)).to.be.true;
80+
81+
circle.setLatLng(Beijing, 2 * radius);
82+
expect(circle.center.lat).to.be.closeTo(Beijing.lat, eps);
83+
expect(circle.center.lng).to.be.closeTo(Beijing.lng, eps);
84+
expect(circle.radius).to.be.closeTo(2 * radius, eps);
85+
});
86+
7587
it("update radius", async function () {
7688
const circle = new GeodesicCircleClass(Seattle, { radius: radius }).addTo(map);
7789
expect(circle).to.be.instanceOf(GeodesicCircleClass);
@@ -84,6 +96,27 @@ describe("Main functionality", function () {
8496
expect(circle.radius).to.be.closeTo(2 * radius, eps);
8597
});
8698

99+
it("update radius with center", async function () {
100+
const circle = new GeodesicCircleClass(Seattle, { radius: radius }).addTo(map);
101+
expect(circle).to.be.instanceOf(GeodesicCircleClass);
102+
compareObject(circle.options, { ...defaultOptions, ...{ radius: radius } });
103+
expect(map.hasLayer(circle)).to.be.true;
104+
105+
expect(circle.center.lat).to.be.closeTo(Seattle.lat, eps);
106+
expect(circle.center.lng).to.be.closeTo(Seattle.lng, eps);
107+
circle.setRadius(2 * radius, Beijing);
108+
expect(circle.radius).to.be.closeTo(2 * radius, eps);
109+
expect(circle.center.lat).to.be.closeTo(Beijing.lat, eps);
110+
expect(circle.center.lng).to.be.closeTo(Beijing.lng, eps);
111+
});
112+
113+
it("Add non-wrapped circle", function () {
114+
const circle = new GeodesicCircleClass(Beijing, { steps: 48, wrap: false }).addTo(map);
115+
expect(circle).to.be.instanceOf(GeodesicCircleClass);
116+
compareObject(circle.options, { ...defaultOptions, ...{ steps: 48, wrap: false } });
117+
expect(map.hasLayer(circle)).to.be.true;
118+
});
119+
87120
it("distance function (wrapper for vincenty inverse)", function () {
88121
const circle = new GeodesicCircleClass(FlindersPeak);
89122
const res = circle.distanceTo(Buninyong);

spec/geodesic-geom-circle.test.ts

Lines changed: 77 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -46,39 +46,80 @@ describe("circle function", function () {
4646
});
4747
});
4848

49-
// describe("splitCircle function", function () {
50-
// it("splitting at antimeridian (west)", function () {
51-
// const fixture: L.LatLngLiteral[][] = [
52-
// [
53-
// { lat: 61.479983, lng: 13.349999 },
54-
// { lat: 56.219051, lng: 27.392553 },
55-
// { lat: 47.396791, lng: 24.852190 },
56-
// { lat: 43.506398, lng: 13.350000 },
57-
// { lat: 47.396791, lng: 1.847809 },
58-
// { lat: 56.219051, lng: -0.692553 }]];
59-
60-
// const circle = geom.circle({ lat: 0, lng: -170 }, 3000 * 1000);
61-
// console.log(circle);
62-
// const split = geom.splitCircle(circle);
63-
// console.log(split);
64-
// // checkFixture([circle], fixture);
65-
// });
66-
67-
// it("splitting at antimeridian (east)", function () {
68-
// const fixture: L.LatLngLiteral[][] = [
69-
// [
70-
// { lat: 61.479983, lng: 13.349999 },
71-
// { lat: 56.219051, lng: 27.392553 },
72-
// { lat: 47.396791, lng: 24.852190 },
73-
// { lat: 43.506398, lng: 13.350000 },
74-
// { lat: 47.396791, lng: 1.847809 },
75-
// { lat: 56.219051, lng: -0.692553 }]];
76-
77-
// const circle = geom.circle({ lat: 0, lng: 170 }, 3000 * 1000);
78-
// const split = geom.splitCircle(circle);
79-
// console.log(circle);
80-
// console.log(split);
81-
// // checkFixture([circle], fixture);
82-
// });
83-
84-
// });
49+
describe("splitCircle function", function () {
50+
it("splitting at antimeridian (west)", function () {
51+
const fixture: L.LatLngLiteral[][] = [
52+
[{ lat: -21.992404660096888, lng: 180 },
53+
{ lat: -13.180500382803578, lng: 166.23330500967688 },
54+
{ lat: 13.180500382803567, lng: 166.23330500967688 },
55+
{ lat: 21.912163313335537, lng: 180 }],
56+
[{ lat: 21.912163313335537, lng: -180 },
57+
{ lat: 27.11163550749344, lng: -170 },
58+
{ lat: 27.11163550749344, lng: -170 },
59+
{ lat: 13.180500382803567, lng: -146.2333050096769 },
60+
{ lat: -13.180500382803556, lng: -146.2333050096769 },
61+
{ lat: -27.11163550749344, lng: -170 },
62+
{ lat: -21.992404660096888, lng: -180 }]
63+
];
64+
const circle = geom.circle(new L.LatLng(0, -170), 3000 * 1000);
65+
const split = geom.splitCircle(circle);
66+
checkFixture(split, fixture);
67+
});
68+
69+
it("splitting at antimeridian (east)", function () {
70+
const fixture: L.LatLngLiteral[][] = [
71+
[{ lat: 21.9924046601208, lng: -180 },
72+
{ lat: 13.180500382803567, lng: -166.23330500967688 },
73+
{ lat: -13.180500382803556, lng: -166.23330500967688 },
74+
{ lat: -21.912163426550492, lng: -180 }],
75+
[{ lat: -21.912163426550492, lng: 180 },
76+
{ lat: -27.11163550749344, lng: 170 },
77+
{ lat: -13.180500382803578, lng: 146.2333050096769 },
78+
{ lat: 13.180500382803567, lng: 146.2333050096769 },
79+
{ lat: 27.11163550749344, lng: 170 },
80+
{ lat: 27.11163550749344, lng: 170 },
81+
{ lat: 21.9924046601208, lng: 180 }]
82+
];
83+
const circle = geom.circle(new L.LatLng(0, 170), 3000 * 1000);
84+
const split = geom.splitCircle(circle);
85+
checkFixture(split, fixture);
86+
});
87+
88+
it("splitting on antimeridian (west)", function () {
89+
const fixture: L.LatLngLiteral[][] = [
90+
[{ lat: -26.58540067640274, lng: 179.99996878961207 },
91+
{ lat: -12.69836235379589, lng: 156.28174493726942 },
92+
{ lat: 13.662436754820993, lng: 156.18289585552634 },
93+
{ lat: 27.563243310965472, lng: 180 }],
94+
[{ lat: 27.563243310965472, lng: -180 },
95+
{ lat: 27.637816560707744, lng: -180 },
96+
{ lat: 27.637816560707744, lng: -180 },
97+
{ lat: 13.662436754820993, lng: -156.18289585552634 },
98+
{ lat: -12.698362353795874, lng: -156.28174493726945 },
99+
{ lat: -26.58541503069416, lng: -180 },
100+
{ lat: -26.58540067640274, lng: -180.000031 }]
101+
];
102+
const circle = geom.circle(new L.LatLng(0.5273, -180), 3000 * 1000);
103+
const split = geom.splitCircle(circle);
104+
checkFixture(split, fixture);
105+
});
106+
107+
it("splitting on antimeridian (east)", function () {
108+
const fixture: L.LatLngLiteral[][] = [
109+
[{ lat: 27.637814866657912, lng: -179.999996 },
110+
{ lat: 13.662436754820993, lng: -156.18289585552634 },
111+
{ lat: -12.698362353795874, lng: -156.28174493726945 },
112+
{ lat: -26.510122528165866, lng: -180 }],
113+
[{ lat: -26.510122528165866, lng: 180 },
114+
{ lat: -26.58541503069416, lng: 180 },
115+
{ lat: -12.69836235379589, lng: 156.28174493726945 },
116+
{ lat: 13.662436754820993, lng: 156.18289585552634 },
117+
{ lat: 27.637816560707744, lng: 180 },
118+
{ lat: 27.637816560707744, lng: 180 },
119+
{ lat: 27.637814866657912, lng: 180.000004 }]
120+
];
121+
const circle = geom.circle(new L.LatLng(0.5273, 180), 3000 * 1000);
122+
const split = geom.splitCircle(circle);
123+
checkFixture(split, fixture);
124+
});
125+
});

spec/geodesic-geom.test.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,6 @@ const geom = new GeodesicGeometry();
7373
describe("constructor and properties", function () {
7474
it("no additional settings given", function () {
7575
expect(geom.steps).to.be.equal(3);
76-
expect(geom.options.steps).to.be.equal(3);
77-
expect(geom.options.wrap).to.be.true;
7876
});
7977
});
8078

@@ -183,8 +181,8 @@ describe("splitLine function", function () {
183181

184182
it("Over Northpole", function () {
185183
const fixture: L.LatLngLiteral[][] = [
186-
[LosAngeles, { lat: 89.75831966628218, lng: -179.99999999974688 }],
187-
[{ lat: 89.75831966628218, lng: 180.00000000025312 }, { lat: 65.3668, lng: 62.2266 }]
184+
[LosAngeles, { lat: 89.75831966628218, lng: -180 }],
185+
[{ lat: 89.75831966628218, lng: 180 }, { lat: 65.3668, lng: 62.2266 }]
188186
];
189187
const split = geom.splitLine(LosAngeles, new L.LatLng(65.3668, 62.2266));
190188
checkFixture(split, fixture);
@@ -246,8 +244,8 @@ describe("splitLine function", function () {
246244

247245
it("Sydney -> LosAngeles (shifted east)", function () {
248246
const fixture: L.LatLngLiteral[][] = [
249-
[Sydney, { lat: -15.09323198441759, lng: 179.99999999997758 }],
250-
[{ lat: -15.09323198441759, lng: -180.00000000002242 }, LosAngeles]
247+
[Sydney, { lat: -15.09323198441759, lng: 180 }],
248+
[{ lat: -15.09323198441759, lng: -180 }, LosAngeles]
251249
];
252250
const split = geom.splitLine(new L.LatLng(Sydney.lat, Sydney.lng), new L.LatLng(LosAngeles.lat, LosAngeles.lng + 360));
253251
checkFixture(split, fixture);
@@ -267,6 +265,18 @@ describe("splitLine - test cases for bugs #1", function () {
267265
const split = geom.splitLine(LosAngeles, new L.LatLng(36.597887451521956, 129.52500015633));
268266
checkFixture(split, fixture);
269267
});
268+
269+
it("Line starts exactly on antimeridian", function () {
270+
const fixture: L.LatLngLiteral[][] = [
271+
[{ lat: 27.637816560707744, lng: 180 },
272+
{ lat: 27.637814, lng: 180.000003 }],
273+
[{ lat: 27.637814, lng: -179.999996 },
274+
{ lat: 13.662436, lng: -156.182895 }]
275+
];
276+
277+
const split = geom.splitLine(new L.LatLng(27.637816560707744, 180), new L.LatLng(13.662436754820993, 203.81710414447366));
278+
checkFixture(split, fixture);
279+
});
270280
});
271281

272282

0 commit comments

Comments
 (0)