Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mark geospatial tile sources. #512

Merged
merged 1 commit into from
Jan 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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