Skip to content

Commit

Permalink
Mark geospatial tile sources.
Browse files Browse the repository at this point in the history
This adds a check if a path is likely to be a geospatial file, and, if
so, prefers geospatial tile sources over non-geospatial sources.

This also adds a `canRead` function to the base library.
  • Loading branch information
manthey committed Jan 15, 2021
1 parent b7ff3c8 commit 7ac22c1
Show file tree
Hide file tree
Showing 8 changed files with 123 additions and 9 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@

## Unreleased

### Features
- Added a `canRead` method to the core module (#512)

### Improvements
- Better release bioformats resources (#502)
- Better handling of tiff files with JPEG compression and RGB colorspace (#503)
- The openjpeg tile source can decode with parallelism (#511)
- Geospatial tile sources are preferred for geospatial files (#512)

### Bug Fixes
- Harden updates of the item view after making a large image (#508)
Expand Down
2 changes: 1 addition & 1 deletion large_image/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from pkg_resources import DistributionNotFound, get_distribution

from . import tilesource # noqa
from .tilesource import getTileSource # noqa
from .tilesource import canRead, getTileSource # noqa


try:
Expand Down
76 changes: 68 additions & 8 deletions large_image/tilesource/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,30 @@
AvailableTileSources = {}


def isGeospatial(path):
"""
Check if a path is likely to be a geospatial file.
:params path: The path to the file
:returns: True if geospatial.
"""
try:
from osgeo import gdal
from osgeo import gdalconst
except ImportError:
# TODO: log a warning
return False
try:
ds = gdal.Open(path, gdalconst.GA_ReadOnly)
except Exception:
return False
if ds.GetProjection():
return True
if ds.GetDriver().ShortName in {'NITF', 'netCDF'}:
return True
return False


def loadTileSources(entryPointName='large_image.source', sourceDict=AvailableTileSources):
"""
Load all tilesources from entrypoints and add them to the
Expand All @@ -34,7 +58,7 @@ def loadTileSources(entryPointName='large_image.source', sourceDict=AvailableTil
pass


def getTileSourceFromDict(availableSources, pathOrUri, *args, **kwargs):
def getSourceNameFromDict(availableSources, pathOrUri, *args, **kwargs):
"""
Get a tile source based on a ordered dictionary of known sources and a path
name or URI. Additional parameters are passed to the tile source and can
Expand All @@ -43,12 +67,15 @@ def getTileSourceFromDict(availableSources, pathOrUri, *args, **kwargs):
:param availableSources: an ordered dictionary of sources to try.
:param pathOrUri: either a file path or a fixed source via
large_image://<source>.
:returns: a tile source instance or and error.
:returns: the name of a tile source that can read the input, or None if
there is no such source.
"""
sourceObj = pathOrUri
uriWithoutProtocol = pathOrUri.split('://', 1)[-1]
isLargeImageUri = pathOrUri.startswith('large_image://')
extensions = [ext.lower() for ext in os.path.basename(uriWithoutProtocol).split('.')[1:]]
properties = {
'geospatial': isGeospatial(pathOrUri),
}
sourceList = []
for sourceName in availableSources:
sourceExtensions = availableSources[sourceName].extensions
Expand All @@ -60,10 +87,29 @@ def getTileSourceFromDict(availableSources, pathOrUri, *args, **kwargs):
priority = SourcePriority.NAMED
if priority >= SourcePriority.MANUAL:
continue
sourceList.append((priority, sourceName))
for _priority, sourceName in sorted(sourceList):
if availableSources[sourceName].canRead(sourceObj, *args, **kwargs):
return availableSources[sourceName](sourceObj, *args, **kwargs)
propertiesClash = any(
getattr(availableSources[sourceName], k, False) != v
for k, v in properties.items())
sourceList.append((propertiesClash, priority, sourceName))
for _clash, _priority, sourceName in sorted(sourceList):
if availableSources[sourceName].canRead(pathOrUri, *args, **kwargs):
return sourceName


def getTileSourceFromDict(availableSources, pathOrUri, *args, **kwargs):
"""
Get a tile source based on a ordered dictionary of known sources and a path
name or URI. Additional parameters are passed to the tile source and can
be used for properties such as encoding.
:param availableSources: an ordered dictionary of sources to try.
:param pathOrUri: either a file path or a fixed source via
large_image://<source>.
:returns: a tile source instance or and error.
"""
sourceName = getSourceNameFromDict(availableSources, pathOrUri, *args, **kwargs)
if sourceName:
return availableSources[sourceName](pathOrUri, *args, **kwargs)
raise TileSourceException('No available tilesource for %s' % pathOrUri)


Expand All @@ -79,10 +125,24 @@ def getTileSource(*args, **kwargs):
return getTileSourceFromDict(AvailableTileSources, *args, **kwargs)


def canRead(*args, **kwargs):
"""
Check if large_image can read a path or uri.
:returns: True if any appropriate source reports it can read the path or
uri.
"""
if not len(AvailableTileSources):
loadTileSources()
if getSourceNameFromDict(AvailableTileSources, *args, **kwargs):
return True
return False


__all__ = [
'TileSource', 'FileTileSource',
'exceptions', 'TileGeneralException', 'TileSourceException', 'TileSourceAssetstoreException',
'TileOutputMimeTypes', 'TILE_FORMAT_IMAGE', 'TILE_FORMAT_PIL', 'TILE_FORMAT_NUMPY',
'AvailableTileSources', 'getTileSource', 'nearPowerOfTwo',
'AvailableTileSources', 'getTileSource', 'canRead', 'getSourceNameFromDict', 'nearPowerOfTwo',
'etreeToDict', 'dictToEtree',
]
1 change: 1 addition & 0 deletions sources/gdal/large_image_source_gdal/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ class GDALFileTileSource(FileTileSource):
'image/tiff': SourcePriority.LOW,
'image/x-tiff': SourcePriority.LOW,
}
geospatial = True

def __init__(self, path, projection=None, unitsPerPixel=None, **kwargs):
"""
Expand Down
1 change: 1 addition & 0 deletions sources/mapnik/large_image_source_mapnik/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class MapnikFileTileSource(GDALFileTileSource):
'image/tiff': SourcePriority.LOWER,
'image/x-tiff': SourcePriority.LOWER,
}
geospatial = True

def __init__(self, path, projection=None, unitsPerPixel=None, **kwargs):
"""
Expand Down
1 change: 1 addition & 0 deletions test/data/04091217_ruc.nc.sha512
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3380c8de64afc46a5cfe764921e0fd5582380b1065754a6e6c4fa506625ee26edb1637aeda59c1d2a2dc245364b191563d8572488c32dcafe9e706e208fd9939
13 changes: 13 additions & 0 deletions test/test_source_base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
# -*- coding: utf-8 -*-
import os

import large_image
from large_image.tilesource import nearPowerOfTwo

from . import utilities


def testNearPowerOfTwo():
assert nearPowerOfTwo(45808, 11456)
Expand All @@ -11,3 +15,12 @@ def testNearPowerOfTwo():
assert not nearPowerOfTwo(45808, 11400, 0.005)
assert nearPowerOfTwo(45808, 11500)
assert not nearPowerOfTwo(45808, 11500, 0.005)


def testCanRead():
testDir = os.path.dirname(os.path.realpath(__file__))
imagePath = os.path.join(testDir, 'test_files', 'yb10kx5k.png')
assert large_image.canRead(imagePath) is False

imagePath = utilities.externaldata('data/sample_image.ptif.sha512')
assert large_image.canRead(imagePath) is True
34 changes: 34 additions & 0 deletions test/test_source_mapnik.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import pytest
import six

import large_image
from large_image.exceptions import TileSourceException

import large_image_source_mapnik
Expand Down Expand Up @@ -332,3 +333,36 @@ def testGuardAgainstBadLatLong():
assert bounds['xmax'] == 179.99583333
assert bounds['ymin'] == -89.99583333
assert bounds['ymax'] == 90


def testTileFromNetCDF():
imagePath = utilities.externaldata('data/04091217_ruc.nc.sha512')
source = large_image_source_mapnik.MapnikFileTileSource(imagePath)
tileMetadata = source.getMetadata()

assert tileMetadata['tileWidth'] == 256
assert tileMetadata['tileHeight'] == 256
assert tileMetadata['sizeX'] == 93
assert tileMetadata['sizeY'] == 65
assert tileMetadata['levels'] == 1
assert tileMetadata['bounds']['srs'].strip() == '+init=epsg:4326'
assert tileMetadata['geospatial']

# Getting the metadata with a specified projection will be different
source = large_image_source_mapnik.MapnikFileTileSource(
imagePath, projection='EPSG:3857')
tileMetadata = source.getMetadata()

assert tileMetadata['tileWidth'] == 256
assert tileMetadata['tileHeight'] == 256
assert tileMetadata['sizeX'] == 512
assert tileMetadata['sizeY'] == 512
assert tileMetadata['levels'] == 2
assert tileMetadata['bounds']['srs'] == '+init=epsg:3857'
assert tileMetadata['geospatial']


def testTileSourceFromNetCDF():
imagePath = utilities.externaldata('data/04091217_ruc.nc.sha512')
ts = large_image.getTileSource(imagePath)
assert 'mapnik' in ts.name

0 comments on commit 7ac22c1

Please sign in to comment.