diff --git a/examples/Projects.ipynb b/examples/Projects.ipynb index fb28d62..0c8ac96 100644 --- a/examples/Projects.ipynb +++ b/examples/Projects.ipynb @@ -50,7 +50,9 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "# Change the index in projects[3] to a value within your list of projects\n", @@ -72,14 +74,136 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "# Change the index in projects[4] to a value within your list of projects\n", "project2 = projects[4]\n", "m2 = project2.get_map()\n", "project1.compare(project2, m2)\n", - "m2." + "m2" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Project export" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Export as PNG" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true, + "scrolled": true + }, + "outputs": [], + "source": [ + "from IPython.display import Image\n", + "project = api.projects[3]\n", + "bbox = '-121.726057,37.278423,-121.231672,37.377250'\n", + "Image(project.png(bbox))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "Image(project.png(bbox, zoom=15))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Export as GeoTIFF" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Display in the notebook\n", + "\n", + "Note: this example requires\n", + "[`numpy`](http://www.numpy.org/),\n", + "[`matplotlib`](http://matplotlib.org/), and a fairly recent version of\n", + "[`rasterio`](https://mapbox.github.io/rasterio/).\n", + "\n", + "If you don't have them, you can run the cell at the bottom of this notebook,\n", + "provided your pip installation directory is writable." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from rasterio.io import MemoryFile\n", + "import matplotlib.pyplot as plt\n", + "project = api.projects[3]\n", + "bbox = '-121.726057,37.278423,-121.231672,37.377250'\n", + "data = project.geotiff(bbox, zoom=17)\n", + "\n", + "with MemoryFile(data) as memfile:\n", + " with memfile.open() as dataset:\n", + " plt.imshow(dataset.read(1), cmap='RdBu')\n", + " \n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Save as a file" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "project = api.projects[3]\n", + "bbox = '-121.726057,37.278423,-121.231672,37.377250'\n", + "data = project.geotiff(bbox, zoom=17)\n", + "with open('sample.tiff', 'wb') as outf:\n", + " outf.write(data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Installs" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "%%bash\n", + "pip install numpy matplotlib rasterio==1.0a8" ] } ], diff --git a/rasterfoundry/api.py b/rasterfoundry/api.py index e035823..004512e 100644 --- a/rasterfoundry/api.py +++ b/rasterfoundry/api.py @@ -5,7 +5,7 @@ from bravado.swagger_model import load_file from simplejson import JSONDecodeError -from models import Project +from models import Project, MapToken from exceptions import RefreshTokenException @@ -72,6 +72,29 @@ def get_api_token(self, refresh_token): raise RefreshTokenException('Error using refresh token, please ' 'verify it is valid') + @property + def map_tokens(self): + """List map tokens a user has access to + + Returns: + List[MapToken] + """ + + has_next = True + page = 0 + map_tokens = [] + while has_next: + paginated_map_tokens = ( + self.client.Imagery.get_map_tokens(page=page).result() + ) + map_tokens += [ + MapToken(map_token, self) + for map_token in paginated_map_tokens.results + ] + page = paginated_map_tokens.page + 1 + has_next = paginated_map_tokens.hasNext + return map_tokens + @property def projects(self): """List projects a user has access to diff --git a/rasterfoundry/models/__init__.py b/rasterfoundry/models/__init__.py index 93456c0..d1c04b6 100644 --- a/rasterfoundry/models/__init__.py +++ b/rasterfoundry/models/__init__.py @@ -1 +1,2 @@ from .project import Project # NOQA +from .map_token import MapToken # NOQA diff --git a/rasterfoundry/models/map_token.py b/rasterfoundry/models/map_token.py new file mode 100644 index 0000000..0089893 --- /dev/null +++ b/rasterfoundry/models/map_token.py @@ -0,0 +1,23 @@ +class MapToken(object): + """A Raster Foundry map token""" + + def __repr__(self): + return ''.format(self.project.name, self.token) + + def __init__(self, map_token, api): + """Instantiate a new MapToken + + Args: + map_token (MapToken): generated MapToken object from specification + api (API): api used to make requests + """ + + self._map_token = map_token + self.api = api + + # A few things we care about + self.token = map_token.id + self.last_modified = map_token.modifiedAt + self.project = [ + proj for proj in self.api.projects if proj.id == map_token.project + ].pop() diff --git a/rasterfoundry/models/project.py b/rasterfoundry/models/project.py index fd3746d..31784db 100644 --- a/rasterfoundry/models/project.py +++ b/rasterfoundry/models/project.py @@ -1,5 +1,9 @@ """A Project is a collection of zero or more scenes""" +import requests + from .. import NOTEBOOK_SUPPORT +from ..decorators import check_notebook +from .map_token import MapToken if NOTEBOOK_SUPPORT: from ipyleaflet import ( @@ -8,13 +12,12 @@ TileLayer, ) -from ..decorators import check_notebook # NOQA - class Project(object): """A Raster Foundry project""" TILE_PATH_TEMPLATE = '/tiles/{id}/{{z}}/{{x}}/{{y}}/' + EXPORT_TEMPLATE = '/tiles/{project}/export/' def __repr__(self): return ''.format(self.name) @@ -33,22 +36,110 @@ def __init__(self, project, api): self.name = project.name self.id = project.id - @check_notebook - def get_map(self, **kwargs): - """Return an ipyleaflet map centered on this project's center + def get_center(self): + """Get the center of this project's extent""" + coords = self._project.extent.get('coordinates') + if not coords: + raise ValueError( + 'Project must have coordinates to calculate a center' + ) + x_min = min( + coord[0] + (360 if coord[0] < 0 else 0) for coord in coords[0] + ) + x_max = max( + coord[0] + (360 if coord[0] < 0 else 0) for coord in coords[0] + ) + y_min = min(coord[1] for coord in coords[0]) + y_max = max(coord[1] for coord in coords[0]) + center = [(y_min + y_max) / 2., (x_min + x_max) / 2.] + if center[0] > 180: + center[0] = center[0] - 360 + return tuple(center) + + def get_map_token(self): + """Returns the map token for this project + + Returns: + str + """ + + resp = ( + self.api.client.Imagery.get_map_tokens(project=self.id).result() + ) + if resp.results: + return MapToken(resp.results[0], self.api) + + def get_export(self, bbox, zoom=10, export_format='png'): + """Download this project as a file + + PNGs will be returned if the export_format is anything other than tiff Args: - **kwargs: additional arguments to pass to Map initializations + bbox (str): GeoJSON format bounding box for the download + export_format (str): Requested download format + + Returns: + str """ - default_url = ( - 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/' - 'light_all/{z}/{x}/{y}.png' + + headers = self.api.http.session.headers.copy() + headers['Accept'] = 'image/{}'.format( + export_format + if export_format.lower() in ['png', 'tiff'] + else 'png' ) - return Map( - default_tiles=TileLayer(url=kwargs.get('url', default_url)), - center=self.get_center(), - scroll_wheel_zoom=kwargs.get('scroll_wheel_zoom', True), - **kwargs + export_path = self.EXPORT_TEMPLATE.format(project=self.id) + request_path = '{scheme}://{host}{export_path}'.format( + scheme=self.api.scheme, host=self.api.tile_host, + export_path=export_path + ) + map_token = self.get_map_token() + if not map_token: + raise "Project {} has not yet been shared".format(self.id) + + return requests.get( + request_path, + params={'bbox': bbox, 'zoom': zoom, 'mapToken': map_token.token}, + headers=headers + ) + + def geotiff(self, bbox, zoom=10): + """Download this project as a geotiff + + The returned string is the raw bytes of the associated geotiff. + + Args: + bbox (str): GeoJSON format bounding box for the download + zoom (int): zoom level for the export + + Returns: + str + """ + + return self.get_export(bbox, zoom, 'tiff').content + + def png(self, bbox, zoom=10): + """Download this project as a png + + The returned string is the raw bytes of the associated png. + + Args: + bbox (str): GeoJSON format bounding box for the download + zoom (int): zoom level for the export + + Returns + str + """ + + return self.get_export(bbox, zoom, 'png').content + + def tms(self): + """Return a TMS URL for a project""" + + tile_path = self.TILE_PATH_TEMPLATE.format(id=self.id) + return '{scheme}://{host}{tile_path}'.format( + scheme=self.api.scheme, host=self.api.tile_host, + tile_path=tile_path ) @check_notebook @@ -77,36 +168,25 @@ def compare(self, other, leaflet_map): ) leaflet_map.add_control(control) - def get_center(self): - """Get the center of this project's extent""" - coords = self._project.extent.get('coordinates') - if not coords: - raise ValueError( - 'Project must have coordinates to calculate a center' - ) - x_min = min( - coord[0] + (360 if coord[0] < 0 else 0) for coord in coords[0] - ) - x_max = max( - coord[0] + (360 if coord[0] < 0 else 0) for coord in coords[0] - ) - y_min = min(coord[1] for coord in coords[0]) - y_max = max(coord[1] for coord in coords[0]) - center = [(y_min + y_max) / 2., (x_min + x_max) / 2.] - if center[0] > 180: - center[0] = center[0] - 360 - return tuple(center) - @check_notebook def get_layer(self): """Returns a TileLayer for display using ipyleaflet""" return TileLayer(url=self.tms()) - def tms(self): - """Return a TMS URL for a project""" + @check_notebook + def get_map(self, **kwargs): + """Return an ipyleaflet map centered on this project's center - tile_path = self.TILE_PATH_TEMPLATE.format(id=self.id) - return '{scheme}://{host}{tile_path}'.format( - scheme=self.api.scheme, host=self.api.tile_host, - tile_path=tile_path + Args: + **kwargs: additional arguments to pass to Map initializations + """ + default_url = ( + 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/' + 'light_all/{z}/{x}/{y}.png' + ) + return Map( + default_tiles=TileLayer(url=kwargs.get('url', default_url)), + center=self.get_center(), + scroll_wheel_zoom=kwargs.get('scroll_wheel_zoom', True), + **kwargs ) diff --git a/rasterfoundry/spec.yml b/rasterfoundry/spec.yml index aa7041e..3485bb9 100644 --- a/rasterfoundry/spec.yml +++ b/rasterfoundry/spec.yml @@ -984,6 +984,7 @@ paths: tags: - Imagery parameters: + - $ref: '#/parameters/page' - $ref: '#/parameters/projectId' - $ref: '#/parameters/organization' - $ref: '#/parameters/createdBy' @@ -1642,7 +1643,7 @@ parameters: type: string format: uuid projectId: - name: projectId + name: project in: query description: UUID for project type: string @@ -1977,13 +1978,8 @@ definitions: type: string description: Human friendly label for map token project: - type: object - properties: - id: - type: string - format: UUID - name: - type: string + type: string + format: uuid MapTokenPaginated: allOf: - $ref: '#/definitions/PaginatedResponse' diff --git a/scripts/update b/scripts/update index 58273aa..2bc5070 100755 --- a/scripts/update +++ b/scripts/update @@ -12,9 +12,17 @@ virtualenv venv source venv/bin/activate pip install jupyter notebook jupyter nbextension enable --py --sys-prefix widgetsnbextension - pip install -e git+https://github.com/azavea/ipyleaflet#egg=9cfd238 +# MacOS has some sort of messy openSSL incompatibility +# See https://github.com/pyca/cryptography/issues/3489 +if [ "$(uname -s)" == "Darwin" ]; then + pip install cryptography \ + --global-option=build_ext \ + --global-option="-L/usr/local/opt/openssl/lib" \ + --global-option="-I/usr/local/opt/openssl/include" +fi + jupyter nbextension install --py --symlink --sys-prefix ipyleaflet jupyter nbextension enable --py --sys-prefix ipyleaflet diff --git a/setup.py b/setup.py index faf7221..6e8e777 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ packages=setuptools.find_packages(exclude=['tests']), package_data={'': ['*.yml']}, install_requires=[ - 'cryptography >= 1.3.2', + 'cryptography == 1.8.1', 'pyasn1 >= 0.2.3', 'requests >= 2.9.1', 'bravado >= 8.4.0'