Skip to content

Commit

Permalink
feature(Parser): add parsers for GTX, ISG and GDF file formats
Browse files Browse the repository at this point in the history
  • Loading branch information
mgermerie committed Dec 17, 2021
1 parent 38569f6 commit a55b154
Show file tree
Hide file tree
Showing 9 changed files with 416 additions and 1 deletion.
5 changes: 4 additions & 1 deletion docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,10 @@
"CameraCalibrationParser",
"B3dmParser",
"ShapefileParser",
"LASParser"
"LASParser",
"GTXParser",
"GDFParser",
"ISGParser"
],

"Converter": [
Expand Down
3 changes: 3 additions & 0 deletions src/Main.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ export { default as KMLParser } from 'Parser/KMLParser';
export { default as CameraCalibrationParser } from 'Parser/CameraCalibrationParser';
export { default as ShapefileParser } from 'Parser/ShapefileParser';
export { default as LASParser } from 'Parser/LASParser';
export { default as ISGParser } from 'Parser/ISGParser';
export { default as GDFParser } from 'Parser/GDFParser';
export { default as GTXParser } from 'Parser/GTXParser';
export { enableDracoLoader, glTFLoader, legacyGLTFLoader } from 'Parser/B3dmParser';

// 3D Tiles classes and extensions
Expand Down
86 changes: 86 additions & 0 deletions src/Parser/GDFParser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import * as THREE from 'three';
import GeoidGrid from 'Core/Geographic/GeoidGrid';
import Extent from 'Core/Geographic/Extent';
import { BYTES_PER_DOUBLE } from 'Parser/GTXParser';


export function getHeaderAttribute(header, attributeName) {
const attributeRow = header[header.indexOf(header.find(element => element.includes(attributeName)))].split(' ')
.filter(value => value !== '');
return parseFloat(attributeRow[attributeRow.length - 1]);
}


/**
* The `GDFParser` module provides a `[parse]{@link module:GDFParser.parse}` method. This method takes the content of a
* GDF file in, and returns a `{@link GeoidGrid}`. the `{@link GeoidGrid}` contains all the necessary attributes and
* methods to access the GDF data in iTowns.
*
* @module GDFParser
*/
export default {
/**
* Parses a GDF file content and returns a corresponding `{@link GeoidGrid}`.
*
* @param {string} gdf The content of the GDF file to parse.
* @param {Object} options An object gathering the optional parameters to pass to
* the parser.
* @param {Object} [options.in={}] Information on the GDF data.
* @param {string} [options.in.crs='EPSG:4326'] The Coordinates Reference System (CRS) of the GDF data.
* It must be a geographic CRS, and must be given as an EPSG
* code.
*
* @returns {Promise<GeoidGrid>} A promise resolving with a `{@link GeoidGrid}`, which contains all the necessary
* attributes and methods to access GDF file data.
*/
parse(gdf, options = { in: {} }) {
const rows = gdf.split('\n');
const firstMeasureLine = rows.indexOf(rows.find(row => row.includes('end_of_head'))) + 1;
const rawHeaderData = rows.slice(0, firstMeasureLine);

// ---------- GET METADATA FROM THE FILE : ----------

const metadata = {
minX: getHeaderAttribute(rawHeaderData, 'longlimit_west'),
maxX: getHeaderAttribute(rawHeaderData, 'longlimit_east'),
minY: getHeaderAttribute(rawHeaderData, 'latlimit_south'),
maxY: getHeaderAttribute(rawHeaderData, 'latlimit_north'),
stepX: getHeaderAttribute(rawHeaderData, 'gridstep'),
stepY: getHeaderAttribute(rawHeaderData, 'gridstep'),
nRows: getHeaderAttribute(rawHeaderData, 'latitude_parallels'),
nColumns: getHeaderAttribute(rawHeaderData, 'longitude_parallels'),
};

// ---------- BUILD A DATA VIEWER FROM THE TEXT DATA : ----------

const data = new DataView(
new ArrayBuffer(BYTES_PER_DOUBLE * metadata.nRows * metadata.nColumns),
);

let index = 0;
for (let row of rows.slice(firstMeasureLine, rows.length)) {
row = row.split(' ').filter(value => value !== '');

if (!row.length) { continue; }

data.setFloat64(index * BYTES_PER_DOUBLE, parseFloat(row[2]));
index++;
}

// ---------- CREATE A GeoidGrid FOR THE GIVEN FILE DATA : ----------

const dataExtent = new Extent(
options.in.crs || 'EPSG:4326',
metadata.minX, metadata.maxX, metadata.minY, metadata.maxY,
);

const dataStep = new THREE.Vector2(metadata.stepX, metadata.stepY);

const getData = (verticalIndex, horizontalIndex) =>
data.getFloat64((
metadata.nColumns * (metadata.nRows - verticalIndex - 1) + horizontalIndex
) * BYTES_PER_DOUBLE);

return Promise.resolve(new GeoidGrid(dataExtent, dataStep, getData));
},
};
90 changes: 90 additions & 0 deletions src/Parser/GTXParser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import * as THREE from 'three';
import GeoidGrid from 'Core/Geographic/GeoidGrid';
import Extent from 'Core/Geographic/Extent';


export const BYTES_PER_DOUBLE = 8;
export const BYTES_PER_FLOAT = 4;


/**
* The `GTXParser` module provides a `[parse]{@link module:GTXParser.parse}` method. This method takes the content of a
* GTX file in, and returns a `{@link GeoidGrid}`. The `{@link GeoidGrid}` contains all the necessary attributes and
* methods to access the GTX data in iTowns.
*
* @module GTXParser
*/
export default {
/**
* Parses a GTX file content and returns a corresponding `{@link GeoidGrid}`.
*
* @param {ArrayBuffer} gtx The content of the GTX file to parse.
* @param {Object} options An object gathering the optional parameters to pass to
* the parser.
* @param {Object} [options.in={}] Information on the GTX data.
* @param {string} [options.in.crs='EPSG:4326'] The Coordinates Reference System (CRS) of the GTX data.
* It must be a geographic CRS, and must be given as an
* EPSG code.
* @param {string} [options.in.dataType='float'] The encoding of geoid height data within the GTX file.
* Must be `'float'` or `'double'`.
*
* @returns {Promise<GeoidGrid>} A promise resolving with a `{@link GeoidGrid}`, which contains all the necessary
* attributes and methods to access GTX file data.
*/
parse(gtx, options = { in: {} }) {
const dataType = options.in.dataType || 'float';
if (!['float', 'double'].includes(dataType)) {
throw new Error(
'`dataType` parameter is incorrect for GTXParser.parse method. ' +
'This parameter must be either `double` or `float`.',
);
}

// ---------- GET METADATA FROM THE FILE : ----------

const headerView = new DataView(gtx, 0, 40);
const metadata = {
minX: headerView.getFloat64(8),
minY: headerView.getFloat64(0),
stepX: headerView.getFloat64(24),
stepY: headerView.getFloat64(16),
nColumns: headerView.getInt32(36),
nRows: headerView.getInt32(32),
};

// ---------- BUILD A DATA VIEWER : ----------

const dataView = new DataView(gtx, 40);

// ---------- CREATE A GeoidGrid FOR THE GIVEN FILE DATA : ----------

// formula for the max longitude : maxLongitude = minLongitude + deltaLongitude * (nColumns - 1)
const maxX = metadata.minX + metadata.stepX * (metadata.nColumns - 1);
// formula for the max latitude : maxLatitude = minLatitude + deltaLatitude * (nRows - 1)
const maxY = metadata.minY + metadata.stepY * (metadata.nRows - 1);

const dataExtent = new Extent(
options.in.crs || 'EPSG:4326',
metadata.minX, maxX, metadata.minY, maxY,
);

const dataStep = new THREE.Vector2(metadata.stepX, metadata.stepY);

const getData = (verticalIndex, horizontalIndex) => {
// formula to get the index of a geoid height from a latitude and longitude indexes is :
// ``(nColumns * latIndex + lonIndex) * nBytes``, where nBytes stands for the number of bytes geoid
// height data are encoded on.
if (dataType === 'float') {
return dataView.getFloat32(
(metadata.nColumns * verticalIndex + horizontalIndex) * BYTES_PER_FLOAT,
);
} else if (dataType === 'double') {
return dataView.getFloat64(
(metadata.nColumns * verticalIndex + horizontalIndex) * BYTES_PER_DOUBLE,
);
}
};

return Promise.resolve(new GeoidGrid(dataExtent, dataStep, getData));
},
};
81 changes: 81 additions & 0 deletions src/Parser/ISGParser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import * as THREE from 'three';
import GeoidGrid from 'Core/Geographic/GeoidGrid';
import Extent from 'Core/Geographic/Extent';
import { getHeaderAttribute } from 'Parser/GDFParser';
import { BYTES_PER_DOUBLE } from 'Parser/GTXParser';


/**
* The `ISGParser` module provides a `[parse]{@link module:ISGParser.parse}` method. This method takes the content of a
* ISG file in, and returns a `{@link GeoidGrid}`. the `{@link GeoidGrid}` contains all the necessary attributes and
* methods to access the ISG data in iTowns.
*
* @module ISGParser
*/
export default {
/**
* Parses an ISG file content and returns a corresponding `{@link GeoidGrid}`.
*
* @param {string} isg The content of the ISG file to parse.
* @param {Object} options An object gathering the optional parameters to pass to
* the parser.
* @param {Object} [options.in={}] Information on the ISG data.
* @param {string} [options.in.crs='EPSG:4326'] The Coordinates Reference System (CRS) of the ISG data.
* It must be a geographic CRS, and must be given as an EPSG
* code.
*
* @returns {Promise<GeoidGrid>} A promise resolving with a `{@link GeoidGrid}`, which contains all the necessary
* attributes and methods to access ISG file data.
*/
parse(isg, options = { in: {} }) {
const rows = isg.split('\n');
const firstMeasureLine = rows.indexOf(rows.find(row => row.includes('end_of_head'))) + 1;
const rawHeaderData = rows.slice(0, firstMeasureLine);

// ---------- GET METADATA FROM THE FILE : ----------

const metadata = {
minX: getHeaderAttribute(rawHeaderData, 'lon min'),
maxX: getHeaderAttribute(rawHeaderData, 'lon max'),
minY: getHeaderAttribute(rawHeaderData, 'lat min'),
maxY: getHeaderAttribute(rawHeaderData, 'lat max'),
stepX: getHeaderAttribute(rawHeaderData, 'delta lon'),
stepY: getHeaderAttribute(rawHeaderData, 'delta lat'),
nRows: getHeaderAttribute(rawHeaderData, 'nrows'),
nColumns: getHeaderAttribute(rawHeaderData, 'ncols'),
};

// ---------- BUILD A DATA VIEWER FROM THE TEXT DATA : ----------

const data = new DataView(
new ArrayBuffer(BYTES_PER_DOUBLE * metadata.nRows * metadata.nColumns),
);

let index = 0;
for (let row of rows.slice(firstMeasureLine, rows.length)) {
row = row.split(' ').filter(value => value !== '');

if (!row.length) { continue; }

for (const value of row) {
data.setFloat64(index * BYTES_PER_DOUBLE, parseFloat(value));
index++;
}
}

// ---------- CREATE A GeoidGrid FOR THE GIVEN FILE DATA : ----------

const dataExtent = new Extent(
options.in.crs || 'EPSG:4326',
metadata.minX + metadata.stepX / 2, metadata.maxX - metadata.stepX / 2,
metadata.minY + metadata.stepY / 2, metadata.maxY - metadata.stepY / 2,
);

const dataStep = new THREE.Vector2(metadata.stepX, metadata.stepY);

const getData = (verticalIndex, horizontalIndex) =>
data.getFloat64((metadata.nColumns * verticalIndex + horizontalIndex) * BYTES_PER_DOUBLE);

return Promise.resolve(new GeoidGrid(dataExtent, dataStep, getData));
},
};
9 changes: 9 additions & 0 deletions src/Source/Source.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import Extent from 'Core/Geographic/Extent';
import GeoJsonParser from 'Parser/GeoJsonParser';
import KMLParser from 'Parser/KMLParser';
import GDFParser from 'Parser/GDFParser';
import GpxParser from 'Parser/GpxParser';
import GTXParser from 'Parser/GTXParser';
import ISGParser from 'Parser/ISGParser';
import VectorTileParser from 'Parser/VectorTileParser';
import Fetcher from 'Provider/Fetcher';
import Cache from 'Core/Scheduler/Cache';
Expand All @@ -13,6 +16,9 @@ export const supportedFetchers = new Map([
['application/kml', Fetcher.xml],
['application/gpx', Fetcher.xml],
['application/x-protobuf;type=mapbox-vector', Fetcher.arrayBuffer],
['application/gtx', Fetcher.arrayBuffer],
['application/isg', Fetcher.text],
['application/gdf', Fetcher.text],
]);

export const supportedParsers = new Map([
Expand All @@ -21,6 +27,9 @@ export const supportedParsers = new Map([
['application/kml', KMLParser.parse],
['application/gpx', GpxParser.parse],
['application/x-protobuf;type=mapbox-vector', VectorTileParser.parse],
['application/gtx', GTXParser.parse],
['application/isg', ISGParser.parse],
['application/gdf', GDFParser.parse],
]);

const noCache = { getByArray: () => {}, setByArray: a => a, clear: () => {} };
Expand Down
38 changes: 38 additions & 0 deletions test/unit/gdf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import assert from 'assert';
import HttpsProxyAgent from 'https-proxy-agent';
import Fetcher from 'Provider/Fetcher';
import GDFParser from 'Parser/GDFParser';


describe('GDFParser', function () {
let text;

before(async () => {
const networkOptions = process.env.HTTPS_PROXY ? { agent: new HttpsProxyAgent(process.env.HTTPS_PROXY) } : {};
text = await Fetcher.text(
'https://raw.githubusercontent.com/iTowns/iTowns2-sample-data/master/altitude-conversion-grids/' +
'EGM2008.gdf',
networkOptions,
);
});

it('should default `options.in.crs` parameter to `EPSG:4326`', async function () {
const geoidGrid = await GDFParser.parse(text);
assert.strictEqual(geoidGrid.extent.crs, 'EPSG:4326');
});

it('should parse text data into a GeoidGrid', async function () {
const geoidGrid = await GDFParser.parse(text, { in: { crs: 'EPSG:4326' } });
assert.strictEqual(geoidGrid.extent.west, -180);
assert.strictEqual(geoidGrid.extent.east, 180);
assert.strictEqual(geoidGrid.extent.south, -90);
assert.strictEqual(geoidGrid.extent.north, 90);
assert.strictEqual(geoidGrid.step.x, 1);
assert.strictEqual(geoidGrid.step.y, 1);
});

it('should set a correct data reading method for `GeoidGrid`', async function () {
const geoidGrid = await GDFParser.parse(text, { in: { crs: 'EPSG:4326' } });
assert.strictEqual(geoidGrid.getData(geoidGrid.dataSize.y - 2, 1), 13.813008707225);
});
});
Loading

0 comments on commit a55b154

Please sign in to comment.