Code Monkey home page Code Monkey logo

geotiff-geokeys-to-proj4's Introduction

geotiff-geokeys-to-proj4

This library converts GeoTIFF's geokeys to Proj4 string, so you can consume your images.

Intended to be used with geotiff.js and proj4js, it's basically a glue between these libraries, but it can be used with alternatives.

Designed for both frontend and backend. Supports ES3+ environments (any browser from 2000 year). Size is ~1.46 Mb (despite what npm says, it counts both sources and bundle).

Grab it from npm: npm install geotiff-geokeys-to-proj4.

Looking for maintainers!

EPSG updates their database once in a month, these updates needs to be integrated into this library. For now, there's no one to perform this task. If you want to help, please, see how to update the database, create an issue to let me know that we've got a maintainer, and submit pull requests with the updated files.

Demo & example

GeoTIFF 3D DEM Viewer demonstrates how to read GeoTIFF files and display them in CesiumJS as 3D terrain. Check the source code for more. Files that'll interest you the most: initial image setup, where all interesting stuff is.

Still, be sure to check out usage example below!

Usage

Importing this library

In Node.js or with bundler: const geokeysToProj4 = require("geotiff-geokeys-to-proj4");

In TypeScript: import geokeysToProj4 from "geotiff-geokeys-to-proj4"; or import { toProj4, convertCoordinates } from "geotiff-geokeys-to-proj4";

In a browser: <script src="path/to/main-dist-iife.js"></script> - after that you'll have geokeysToProj4 global variable.

Please note that ESM module is not transpiled to ES3. Use a bundler to transpile your app to whatever ES version you need.

General usage

In general, you want to:

  1. Read geokeys.
  2. Pass them to geokeysToProj4.toProj4() (let's call returned object projObj).
  3. Pass projObj.proj4 (which is Proj4 string) to proj4js.
  4. Convert pixel coordinates and pixel value (Z coordinate; DEM or geocentric CRS only; for other files, use any number, it doesn't matter) to CRS coordinates (let's call them crsX, crsY and crsZ).
  5. Convert CRS coordinates to usable units (in most cases, meters or degrees, but GeoTIFF allows speed, angular speed, and scale): geokeysToProj4(crsX, crsY, crsZ, projObj.coordinatesConversionParameters).
  6. The returned object will contain x, y, z coordinates which are ready to be projected with proj4js. So project them.

Of course, you can alter this workflow to use this library with any other (presumably, server-side) software.

Let's demonstrate everything from start to finish on an example with geotiff.js and proj4js:

// Import all the stuff
const geotiff = require("geotiff"); // geotiff.js
const proj4 = require("proj4"); // proj4js
const geokeysToProj4 = require("geotiff-geokeys-to-proj4"); // This library

// Let's wrap our example in a function
async function workWithGeoTIFF(blob) {
    // Read image. See geotiff.js docs on what all of that means.
    const tiff = await geotiff.fromBlob(blob); // Read blob
    const imageCount = await tiff.getImageCount(); // Get image count

    // Work with each image in a file
    for (let i = 0; i < imageCount; i++) {
        const image = await tiff.getImage(i); // Get image instance
        const geoKeys = image.getGeoKeys(); // Get geokeys
        const projObj = geokeysToProj4.toProj4(geoKeys); // Convert geokeys to proj4 string
        // The function above returns an object where proj4 property is a Proj4 string and coordinatesConversionParameters is conversion parameters which we'll use later
        const projection = proj4(projObj.proj4, "WGS84"); // Project our GeoTIFF to WGS84

        // Now you may want to deal with errors. Unfortunately, errors are unavoidable, but in most cases, you can warn the user or just continue on.
        // All occurred errors will be in projObj.errors object. See the docs for more information:
        // https://matafokka.github.io/geotiff-geokeys-to-proj4/module-geokeysToProj4.html#.ConversionErrors__anchor

        // Work with pixels
        // For looping over pixels
        const width = image.getWidth();
        const height = image.getHeight();
        // Pixel dimensions for converting image coordinates to source CRS coordinates
        const [originX, originY] = image.getOrigin();
        const [xSize, ySize] = image.getResolution();

        // Read rows
        for (let y = 0; y < height; y++) {
            // Read one row of pixels. Easier to deal with coordinates, takes less RAM.
            const raster = await image.readRasters({window: [0, y, width, y + 1]});
            const color0 = raster[0]; // Raster is a TypedArray where elements are colors and their elements are pixel values of that color

            // Read columns. Since we're reading full row, we can replace color0.length with width, but I find color0.length more explicit.
            for (let x = 0; i < color0.length; x++) {

                // Convert current pixel's coordinates to CRS by:
                // 1. Multiplying current coordinates by pixel size which will result in distance from top-left corner in CRS units.
                // 2. Adding this value to top-left corner coordinates which will result in "global" coordinates in CRS units.
                // This will work because image is transformed by Affine Transformation which preserves parallelism.
                // Warning: this logic works only for source CRS, target CRS might screw up parallel lines, so pixel dimensions will not be constant!
                const crsX = originX + x * xSize;
                const crsY = originY + y * ySize;

                // DEM or geocentric CRS only: Z coordinate is pixel value. You may want to use another band or a combination of bands as Z coordinate.
                // For other files, you may use any number instead, it doesn't matter.
                const crsZ = color0[i];

                // Check if coordinates are already in meters (or other "standard" units). If not, convert them. Either:

                // 1. Use convertCoordinates():
                let point;

                if (projObj.shouldConvertCoordinates) // You can remove this condition, convertCoordinates() will work just fine in any case. Just a bit of time saving when dealing with large files.
                    point = geokeysToProj4.convertCoordinates(crsX, crsY, crsZ, projObj.coordinatesConversionParameters);
                else
                    point = { x: crsX, y: crsY, z: crsZ };

                // 2. Just multiply manually to speed up execution by removing function calls and conditions:
                point = {
                    x: crsX * projObj.coordinatesConversionParameters.x,
                    y: crsY * projObj.coordinatesConversionParameters.y,
                    z: crsZ * projObj.coordinatesConversionParameters.z,
                }

                let projectedPoint = projection.forward(point); // Project these coordinates
                // Work with projected coordinates...
            }
        }
    }
}

Known issues

I don't know which geokeys should take precedence over which. I did what seems to be logical, but I might be wrong. If you know anything about it, please, create an issue and describe whether I'm wrong (and how to fix it) or right (so I'll remove this text).

Vertical CRS which use local depth are not supported because reference points are needed. Following has been excluded:

  1. Vertical CS: 1049 and 1050.
  2. Vertical CRS: 8378 and 8897.

Vertical datums are not supported at all because mappings are needed. If you have at least some mappings, please, let me know by creating an issue.

Manually updating from EPSG database

Unfortunately, epsg.org doesn't provide public access to their database. You need to register an account, and only then you'll be able to download database. Since all of that is for the users and not for bots, both client and server might change in the future, so there's no point in writing self-update script.

To update:

  1. Clone this repo.
  2. Run npm install on cloned repo.
  3. Set up PostgreSQL server (other RDBMS are not supported). Default configuration should be fine, just create a user and a database for that user.
  4. Head over to here, create an account (if you don't have one) and download PostgreSQL scripts.
  5. Extract downloaded scripts to postgres_prep directory.
  6. Run npm run update-all to update everything and rebuilds the project. See arguments below. Warning: this script utilizes epsg schema and will erase it completely! It's hardcoded and can't be changed. To avoid data loss, create a separate database solely to run this script.

There're actually two scripts: update-all which has been described above and update-existing which will update project files from existing database. Both of these scripts accepts following arguments:

--host          - PostgreSQL host                      - Defaults to "localhost"
--port          - PostgreSQL port                      - Defaults to "5432"
--database      - Database containing "epsg" schema    - Defaults to "postgres"
--user          - PostgreSQL database user             - Defaults to "user"
--password      - Password for the user                - Defaults to "12345"

Arguments passed to npm scripts in following manner: npm run [script name] -- [script arguments], for example: npm run update-all -- --user myuser --password mypassword. Note the -- separator, it should always present when using this library's scripts.

FAQ

Why do I need it?

Every GeoTIFF is bound to some kind of Coordinate Reference System (CRS) which in combination with georeferencing data defines where pixel coordinates are on the Earth. These CRS are quite different from what you can find, let's say, in Leaflet or OpenLayers which uses WGS (which is CRS too) by default.

So you need to convert image coordinates from one CRS to another. proj4js is the best tool to do that. You need to supply an input and output CRS (which Proj4 calls a projection; yes, terminology is quite confusing) to it in form of a string.

For output, you can specify whatever your software is using. But input is defined by geokeys (information embedded into GeoTIFF file). Geokeys are really hard to handle. This library will create an input Proj4 string for you.

How to perform all of that is described in example above.

Without these procedures, you'll get wrong results.

How is it different from epsg-index?

epsg-index only provides projections definitions, GeoTIFF uses more than that.

How does it compare to already existing and battle-proven libraries such as GDAL?

Pros:

  1. Can be used in a browser. There're no alternatives for now except for compiling another library to WebAssembly. Even then this library wins because it can run in ES3 environment (any browser from 2000 year).
  2. Easier to use in Node since this library is written in pure JS. Non-JS libraries requires a wrapper or use of CLI.

Cons:

  1. Libraries such as GDAL are, in fact, already battle-proven. Since this library is new, I'm the sole developer for now, and I'm not a cartographer, there might be some bugs.
  2. Since there're no maintainers to update database, more popular libraries might have newer data, though, it shouldn't make big difference.

To summarize: use this library for a frontend, but existing libraries with a wrapper might be better for a backend;

The example above can be put in a function that takes a callback. Why didn't you do that?

Because:

  1. It's not the goal of this library.
  2. It'll slow down reading which is already quite slow.
  3. You'll still need to understand how all this stuff works and be able to modify the example when needed.

This library produces wrong results!

This library only maps geokeys to their Proj4 definitions and builds a final Proj4 string. It doesn't perform any projections.

If you've encountered a bug, please, take a look at Proj4 string first and compare it to one generated by a professional GIS. If something is fundamentally wrong, it's the issue of this library. Otherwise, there's something wrong with Proj4.

If you're comparing results with what epsg.io says, note that while epsg.io is mostly right, it's not an official data source. For example, epsg.io maps CRS 21780 to +proj=somerc, but the right projection seems to be +proj=omerc. If you're not sure how to verify which Proj4 string is correct, feel free to message me anyway.

Please, don't report redundant parameters (for example, +a and +b that are the same as +ellps provides) as a bug. This behavior simplifies development, increases performance by not making useless comparisons and ensures that the right parameters are used.

Missing +units is also not a bug, you're converting coordinates to meters by using geokeysToProj4.convertCoordinates().

What are the sources for Proj4 strings?

Official database from epsg.org is the main data source. Data is enriched by some additional sources. epsg.io is used to selectively check if Proj4 strings are correct.

Contributing

You can contribute by solving described issues or by maintaining the database.

Related projects

  1. geotiff.js is a library that can read GeoTIFF images.
  2. proj4js is a port of Proj4 to JS.
  3. epsg-index is a machine-readable index of all EPSG coordinate reference systems.
  4. epsg.io is a website that provides all the EPSG stuff mostly in human-readable form, and an API to access it.
  5. geokeys-to-proj4js is a project with the same goal but far from to be finished, and it looks abandoned.

geotiff-geokeys-to-proj4's People

Contributors

matafokka avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

geotiff-geokeys-to-proj4's Issues

Web Mercator (EPSG:3857) not detected correctly

I'm attaching a sample DEM file in webmercator coordinates. geotiff-geokeys-to-proj4 gives a Proj4 string for an ellipsoid, which is incorrect:

+proj=merc +lat_0=0 +lon_0=0 +x_0=0 +y_0=0 +ellps=WGS84 +a=6378137 +b=6356752.314245179 +pm=0 +axis=en +no_defs

gdalinfo gives a correct string:

+proj=merc +a=6378137 +b=6378137 +lat_ts=0 +lon_0=0 +x_0=0 +y_0=0 +k=1 +units=m +nadgrids=@null +wktext +no_defs

These are the geokeys:

{
  "GTModelTypeGeoKey": 1,
  "GTRasterTypeGeoKey": 1,
  "GTCitationGeoKey": "WGS 84 / Pseudo-Mercator",
  "GeogCitationGeoKey": "WGS 84",
  "GeogAngularUnitsGeoKey": 9102,
  "ProjectedCSTypeGeoKey": 3857,
  "ProjLinearUnitsGeoKey": 9001
}

And here is the file:

dem.tif.zip

WGS84 ellipsoid not properly converted

My geotiff file has these geokeys:

GTModelTypeGeoKey: 1
​GTRasterTypeGeoKey: 1
​GeogEllipsoidGeoKey: 7030
​GeogGeodeticDatumGeoKey: 6326
​PCSCitationGeoKey: "Transverse Mercator; WGS84; WGS84"
​ProjCoordTransGeoKey: 1
​ProjFalseEastingGeoKey: 500000
​ProjFalseNorthingGeoKey: -5300000
​ProjLinearUnitsGeoKey: 9001
​ProjNatOriginLatGeoKey: 0
​ProjNatOriginLongGeoKey: 19
​ProjScaleAtNatOriginGeoKey: 0.9993
​ProjectedCSTypeGeoKey: 32767
​ProjectionGeoKey: 32767

When passing it through toProj4, the proj4 string is returned like this:

+proj=tmerc +lat_0=0 +lon_0=19 +k_0=0.9993 +x_0=500000 +y_0=-5300000 +a=6378137 +b=6378137 +no_defs 

First of all, this doesn't look right: when +a and +b are equal, doesn't that mean that the ellipsoid is a sphere?
EPSG:7030 is the WGS84 ellipsoid, which is definitely not a sphere.

Second, passing this string to the proj4 library actually throws an exception:

Incorrect elliptical usage. Try using the +approx option in the proj string, or PROJECTION["Fast_Transverse_Mercator"] in the WKT.

I see that the definition being output is from GeogEllipsoidGeoKey.js, but I don't know why that definition is like that.

In EllipsoidsNamesToProj.js there is a definition for ellipsoid 7030, and it is mapped to "WGS84" - so I would have expected the output to be +ellps=WGS84.

In any case, the mapping seems to be wrong:

https://epsg.io/7030-ellipsoid.gml

clearly states an inverse flattening.

For reference, gdalsrsinfo reports the SRS of the geotiff file like this:

PROJ.4 : +proj=tmerc +lat_0=0 +lon_0=19 +k=0.9993 +x_0=500000 +y_0=-5300000 +datum=WGS84 +units=m +no_defs

OGC WKT2:2018 :
PROJCRS["Transverse Mercator; WGS84; WGS84",
    BASEGEOGCRS["WGS_1984",
        DATUM["World Geodetic System 1984",
            ELLIPSOID["WGS 84",6378137,298.257223563,
                LENGTHUNIT["metre",1]],
            ID["EPSG",6326]],
        PRIMEM["Greenwich",0,
            ANGLEUNIT["unknown",0.0174532925199433]]],
    CONVERSION["Transverse Mercator",
        METHOD["Transverse Mercator",
            ID["EPSG",9807]],
        PARAMETER["Latitude of natural origin",0,
            ANGLEUNIT["degree",0.0174532925199433],
            ID["EPSG",8801]],
        PARAMETER["Longitude of natural origin",19,
            ANGLEUNIT["degree",0.0174532925199433],
            ID["EPSG",8802]],
        PARAMETER["Scale factor at natural origin",0.9993,
            SCALEUNIT["unity",1],
            ID["EPSG",8805]],
        PARAMETER["False easting",500000,
            LENGTHUNIT["metre",1],
            ID["EPSG",8806]],
        PARAMETER["False northing",-5300000,
            LENGTHUNIT["metre",1],
            ID["EPSG",8807]]],
    CS[Cartesian,2],
        AXIS["easting",east,
            ORDER[1],
            LENGTHUNIT["metre",1,
                ID["EPSG",9001]]],
        AXIS["northing",north,
            ORDER[2],
            LENGTHUNIT["metre",1,
                ID["EPSG",9001]]]]

Why omit +units?

Missing +units is also not a bug, you're converting coordinates to meters by using geokeysToProj4.convertCoordinates().

Why did you decide on this format? Probably not the best idea to have a proj4 string that only works with this code

Bug in PCSKeys.js

There seems to be a bug in lines 108 & 109 of EPSG/data/PCSKeys.js:

k.o3.ProjOriginLatGeoKey = k.ProjNatOriginLatGeoKey;
k.o3.ProjScaleAtOriginGeoKey = k.ProjScaleAtNatOriginGeoKey;

It think this should be:

k.o3.ProjOriginLatGeoKey = k.o2.ProjNatOriginLatGeoKey;
k.o3.ProjScaleAtOriginGeoKey = k.o2.ProjScaleAtNatOriginGeoKey;

Critical dependency when using React and Typescript

I get the following warning:

WARNING in ./node_modules/geotiff-geokeys-to-proj4/main-dist.js 235:18-19
Critical dependency: require function is used in a way in which dependencies cannot be statically extracted

When importing and using geotiff-geokeys-to-proj4 in a react typescript project. This blocks PRs from merging with strict merging rules etc.

How to reproduce:

  1. Create a clean Typescript react app using the get started instructions here
    npx create-react-app my-app --template typescript
  2. Install needed packages, here are the versions from my package.json:
"geotiff": "^2.0.5",
"geotiff-geokeys-to-proj4": "^2022.9.7",
  1. Add minial logic for importing and using geotiff-geokeys-to-proj4 in the project. Same as in the Readme instructions.
  2. npm start will produce a more verbose warning message than npm run build

Expected behavior:
Compile without a warning from the import.

I can supply the full minimal project as a repo if you have trouble with reporducing the error.

Error in data in `ProjectionGeoKey.js`

​My geotiff file has the following (somewhat mysterious) geoKeys:

GTCitationGeoKey: "UTM Zone 32, Northern Hemisphere"
​GTModelTypeGeoKey: 1
​GTRasterTypeGeoKey: 1
GeogAngularUnitsGeoKey: 9102
​GeogCitationGeoKey: "GCS Name = GRS 1980(IUGG, 1980)|Datum = unknown|Ellipsoid = GRS80|Primem = Greenwich|"
​GeogEllipsoidGeoKey: 32767
​GeogGeodeticDatumGeoKey: 32767
​GeogInvFlatteningGeoKey: 298.257222101
​GeogPrimeMeridianLongGeoKey: 0
​GeogSemiMajorAxisGeoKey: 6378137
​GeogTOWGS84GeoKey: [ 0, 0, 0 ]
​GeographicTypeGeoKey: 32767
​ProjLinearUnitsGeoKey: 9001
​ProjectedCSTypeGeoKey: 32767
​ProjectionGeoKey: 16032

which convertGeoKeys2proj4 converts to this string:

+proj=tmerc +lat_0=0 +lon_0=0.1570796326794895 +k_0=0.9996 +x_0=500000 +y_0=0 +a=6378137 +b=6356752.314140356 +towgs84=0,0,0, +no_defs 

This is not quite right - the lon_0 value should be 9, as listgeo lists the tiff file as follows:

eotiff_Information:
   Version: 1
   Key_Revision: 1.0
   Tagged_Information:
      ModelTiepointTag (2,3):
         0                 0                 0                
         554006.599009888  6301417.47768047  0                
      ModelPixelScaleTag (1,3):
         0.356706451650175 0.356706451650175 0                
      End_Of_Tags.
   Keyed_Information:
      GTModelTypeGeoKey (Short,1): ModelTypeProjected
      GTRasterTypeGeoKey (Short,1): RasterPixelIsArea
      GTCitationGeoKey (Ascii,33): "UTM Zone 32, Northern Hemisphere"
      GeographicTypeGeoKey (Short,1): User-Defined
      GeogCitationGeoKey (Ascii,86): "GCS Name = GRS 1980(IUGG, 1980)|Datum = unknown|Ellipsoid = GRS80|Primem = Greenwich|"
      GeogGeodeticDatumGeoKey (Short,1): User-Defined
      GeogAngularUnitsGeoKey (Short,1): Angular_Degree
      GeogEllipsoidGeoKey (Short,1): User-Defined
      GeogSemiMajorAxisGeoKey (Double,1): 6378137          
      GeogInvFlatteningGeoKey (Double,1): 298.257222101    
      GeogPrimeMeridianLongGeoKey (Double,1): 0                
      GeogTOWGS84GeoKey (Double,3): 0                0                0                
      ProjectedCSTypeGeoKey (Short,1): User-Defined
      ProjectionGeoKey (Short,1): Proj_UTM_zone_32N
      ProjLinearUnitsGeoKey (Short,1): Linear_Meter
      End_Of_Keys.
   End_Of_Geotiff.

Projection = 16032 (UTM zone 32N)
Projection Method: CT_TransverseMercator
   ProjNatOriginLatGeoKey: 0.000000 (  0d 0' 0.00"N)
   ProjNatOriginLongGeoKey: 9.000000 (  9d 0' 0.00"E)
   ProjScaleAtNatOriginGeoKey: 0.999600
   ProjFalseEastingGeoKey: 500000.000000 m
   ProjFalseNorthingGeoKey: 0.000000 m
TOWGS84: 0,0,0
Projection Linear Units: 9001/metre (1.000000m)

The problem seems to be in data preparation, because the string for 16032 in ProjectionGeoKey.js has the wrong value. On http://epsg.io/16032.gml, the latitude is 9. The problem seems simply a mixup between degrees and radians: 0.1570796326794895 is 9 degrees in radians.

[Further elaboration wanted] GeoTIFF with both GCS and PCS defaults to GCS

I have a Geotiff which have geokeys for both GCS and PCS defined.
It seems like geotiff-geokeys-to-proj4 defaults to GCS without any error message or possbility to explicitly pick the correct one (from what I can tell so far). I need the PCS projection which doesn't seem to be possible to get atm.
I solved it temporarly by overwriting the GCS key with an empty string {""}.

Here is my problematic GeoKey as taken from geotiff.js

GTModelTypeGeoKey: 1
GTRasterTypeGeoKey: 1
GeographicTypeGeoKey: 4326
ProjectedCSTypeGeoKey: 32634

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.