-
Notifications
You must be signed in to change notification settings - Fork 2
/
ElevationProfileGenerator.js
200 lines (178 loc) · 8.25 KB
/
ElevationProfileGenerator.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
import along from "@turf/along";
import length from "@turf/length";
import SphericalMercator from "@mapbox/sphericalmercator"
import TileCover from "@mapbox/tile-cover"
import getPixels from "get-pixels"
const ZOOM_LEVEL = 15;
const TILE_SIZE = 256;
let requiredTilesObjects = [];
let tilePromises = [];
let minKmBetweenPoints = null;
/**
* Takes a GeoJSON LineString and returns a GeoJSON FeatureCollection of points along that LineString. Each point feature has
* an "elevation" property that contains the elevation in meters at that point.
*
* @access public
*
* @param {Object} lineString The GeoJSON LineString feature to be checked.
* @param {number} numberOfPoints The maximum number of points to check along the LineString.
* @param {string} mapboxAccessToken The mapbox access token to be used to retrieve rgb elevation
* tiles.
* @param {number} [minMetersBetweenPoints=undefined] The minimum distance in meters to use between each point on the
* LineString. Leaving this parameter as its default results in
* calculating the approximate pixel distance at the used zoom
* level and using that as the minimum distance. Passing null
* results in no minimum being applied, so numberOfPoints points
* are checked regardless of LineString length.
*
* @return {Promise} A Promise whose result is a GeoJSON FeatureCollection containing
* all checked points along the LineString. The calculated
* elevation is contained in the feature's properties attributes
* with key "elevation".
*/
export default async function getLineStringElevationPoints(lineString,
numberOfPoints,
mapboxAccessToken,
minMetersBetweenPoints = undefined) {
return new Promise((resolve, reject) => {
try {
requiredTilesObjects = [];
tilePromises = [];
if (minMetersBetweenPoints === undefined) {
minKmBetweenPoints = Math.ceil(_getPixelDistanceAtLatitudeInMeters(lineString.geometry.coordinates[0][1]) * 10) / 10000;
} else if (minMetersBetweenPoints === null) {
minKmBetweenPoints = null;
} else {
minKmBetweenPoints = minMetersBetweenPoints / 1000;
}
let pointsToCheck = _getLineStringPoints(lineString, numberOfPoints);
_getTileArrayFromLineString(lineString, ZOOM_LEVEL, ZOOM_LEVEL, mapboxAccessToken);
_addXYZCoordinatesToPointsGeoJSON(pointsToCheck, ZOOM_LEVEL);
Promise.all(tilePromises).then(function () {
_addHeightToPointsGeoJSON(pointsToCheck);
resolve(_getFormattedPointsResult(pointsToCheck));
}).catch(function (error) {
reject(error);
});
} catch (error) {
reject(error)
}
})
}
function _getLineStringPoints(lineString, numberOfPoints) {
let distancesToCheck = _getLineStringDistancesToCheck(lineString, numberOfPoints);
let pointsToCheck = _getPointsToCheck(lineString, distancesToCheck);
return pointsToCheck
}
function _getLineStringDistancesToCheck(lineString, numberOfPoints) {
let lineStringLength = length(lineString, {units: "kilometers"});
let stepDistance = lineStringLength / (numberOfPoints - 1);
if (minKmBetweenPoints !== null && stepDistance < minKmBetweenPoints) {
stepDistance = minKmBetweenPoints;
}
let distances = [];
for (let i = 0; (i < numberOfPoints - 1) && (stepDistance * i < lineStringLength); i++) {
distances.push(stepDistance * i);
}
distances.push(lineStringLength);
return distances
}
function _getPointsToCheck(lineString, distancesToCheck) {
let points = [];
distancesToCheck.forEach(function (distance) {
let feature = along(lineString, distance, {units: "kilometers"});
feature.properties.distanceAlongLine = distance * 1000;
points.push(feature);
});
return points;
}
function _addXYZCoordinatesToPointsGeoJSON(pointsArray, zoomLevel) {
let sphericalMercator = new SphericalMercator({
size: TILE_SIZE
});
pointsArray.forEach(function (pointGeoJSON) {
let pointSMCoordinates = sphericalMercator.px([pointGeoJSON.geometry.coordinates[0], pointGeoJSON.geometry.coordinates[1]], zoomLevel);
pointGeoJSON.properties.smCoordinates = {
x: pointSMCoordinates[0],
y: pointSMCoordinates[1]
}
});
}
function _getTileArrayFromLineString(lineString, minZoom, maxZoom, mapboxAccessToken) {
let requiredTiles2DArray = TileCover.tiles(lineString.geometry, {min_zoom: minZoom, max_zoom: maxZoom});
requiredTiles2DArray.forEach(function (requiredTile, index) {
let x = requiredTile[0];
let y = requiredTile[1];
let z = requiredTile[2];
requiredTilesObjects.push({
coordinates: {
x: x,
y: y,
z: z
},
smCoordinates: {
x: x * TILE_SIZE,
y: y * TILE_SIZE,
},
tileData: null
});
_getTerrainTilePixelArray(x, y, z, index, mapboxAccessToken)
});
}
function _getTerrainTilePixelArray(x, y, z, requiredTilesObjectsIndex, access_token) {
tilePromises.push(_getPromisifiedTileRequest(x, y, z, access_token).then(function (pixels) {
requiredTilesObjects[requiredTilesObjectsIndex].tileData = pixels;
}))
}
function _getPromisifiedTileRequest(x, y, z, access_token) {
return new Promise((resolve, reject) => {
getPixels(`https://api.mapbox.com/v4/mapbox.terrain-rgb/${z}/${x}/${y}.pngraw?access_token=${access_token}`, function (error, pixels) {
if (error) {
reject(error);
}
resolve(pixels);
})
})
}
function _addHeightToPointsGeoJSON(points) {
points.forEach(function (point) {
let matchingTile = requiredTilesObjects.filter(function (tile) {
return _pointIsWithinTile(point.properties.smCoordinates, tile.smCoordinates)
})[0];
let xRelativeToTile = point.properties.smCoordinates.x - matchingTile.smCoordinates.x;
let yRelativeToTile = point.properties.smCoordinates.y - matchingTile.smCoordinates.y;
point.properties.elevation = _calculateHeightFromPixel([
matchingTile.tileData.get(xRelativeToTile, yRelativeToTile, 0),
matchingTile.tileData.get(xRelativeToTile, yRelativeToTile, 1),
matchingTile.tileData.get(xRelativeToTile, yRelativeToTile, 2)
]);
});
}
function _calculateHeightFromPixel(pixelRGBArray) {
let red = pixelRGBArray[0];
let green = pixelRGBArray[1];
let blue = pixelRGBArray[2];
return -10000 + ((red * 256 * 256 + green * 256 + blue) * 0.1);
}
function _pointIsWithinTile(pointSMCoordinates, tileSMCoordinates) {
return pointSMCoordinates.x >= tileSMCoordinates.x
&& pointSMCoordinates.x <= (tileSMCoordinates.x + TILE_SIZE)
&& pointSMCoordinates.y >= tileSMCoordinates.y
&& pointSMCoordinates.y <= (tileSMCoordinates.y + TILE_SIZE)
}
function _getFormattedPointsResult(points) {
let featureCollection = {
type: "FeatureCollection",
features: []
};
points.forEach(function (point) {
let pointCopy = JSON.parse(JSON.stringify(point));
delete pointCopy.properties.smCoordinates;
featureCollection.features.push(pointCopy);
});
return featureCollection
}
function _getPixelDistanceAtLatitudeInMeters(latitude) {
const EQUATORIAL_EARTH_CIRCUMFERENCE = 40075016.686;
return EQUATORIAL_EARTH_CIRCUMFERENCE * (Math.cos(latitude * Math.PI / 180) / Math.pow(2, ZOOM_LEVEL + 8));
}