|
23 | 23 | from . import tile_providers as sources
|
24 | 24 | from . import providers
|
25 | 25 |
|
26 |
| -__all__ = ["bounds2raster", "bounds2img", "warp_tiles", "warp_img_transform", "howmany"] |
| 26 | +import projecttile as pt |
| 27 | + |
| 28 | + |
| 29 | +__all__ = ["bounds2raster", "bounds2img", "warp_tiles", "warp_img_transform", "howmany", "bounds2raster_wmts", "bounds2img_wmts"] |
27 | 30 |
|
28 | 31 |
|
29 | 32 | USER_AGENT = "contextily-" + uuid.uuid4().hex
|
@@ -560,3 +563,199 @@ def _merge_tiles(tiles, arrays):
|
560 | 563 | )
|
561 | 564 |
|
562 | 565 | return img, (west, south, east, north)
|
| 566 | + |
| 567 | + |
| 568 | +def _merge_tiles_wmts(tiles, arrays): |
| 569 | + """ |
| 570 | + Merge a set of tiles into a single array. |
| 571 | +
|
| 572 | + Parameters |
| 573 | + --------- |
| 574 | + tiles : list of mercantile.Tile objects |
| 575 | + The tiles to merge. |
| 576 | + arrays : list of numpy arrays |
| 577 | + The corresponding arrays (image pixels) of the tiles. This list |
| 578 | + has the same length and order as the `tiles` argument. |
| 579 | +
|
| 580 | + Returns |
| 581 | + ------- |
| 582 | + img : np.ndarray |
| 583 | + Merged arrays. |
| 584 | + extent : tuple |
| 585 | + Bounding box [west, south, east, north] of the returned image |
| 586 | + in long/lat. |
| 587 | + """ |
| 588 | + # create (n_tiles x 2) array with column for x and y coordinates |
| 589 | + tile_xys = np.array([(t.x, t.y) for t in tiles]) |
| 590 | + |
| 591 | + # get indices starting at zero |
| 592 | + indices = tile_xys - tile_xys.min(axis=0) |
| 593 | + |
| 594 | + # the shape of individual tile images |
| 595 | + h, w, d = arrays[0].shape |
| 596 | + |
| 597 | + # number of rows and columns in the merged tile |
| 598 | + n_x, n_y = (indices + 1).max(axis=0) |
| 599 | + |
| 600 | + # empty merged tiles array to be filled in |
| 601 | + img = np.zeros((h * n_y, w * n_x, d), dtype=np.uint8) |
| 602 | + |
| 603 | + for ind, arr in zip(indices, arrays): |
| 604 | + x, y = ind |
| 605 | + img[y * h : (y + 1) * h, x * w : (x + 1) * w, :] = arr |
| 606 | + |
| 607 | + bounds = np.array([pt.bounds(t) for t in tiles]) # mt => pt |
| 608 | + west, south, east, north = ( |
| 609 | + min(bounds[:, 0]), |
| 610 | + min(bounds[:, 1]), |
| 611 | + max(bounds[:, 2]), |
| 612 | + max(bounds[:, 3]), |
| 613 | + ) |
| 614 | + |
| 615 | + return img, (west, south, east, north) |
| 616 | + |
| 617 | + |
| 618 | +def _calculate_zoom_wmts(w, s, e, n, provider_bounds): |
| 619 | + """Automatically choose a zoom level given a desired number of tiles. |
| 620 | +
|
| 621 | + Parameters |
| 622 | + ---------- |
| 623 | + w : float |
| 624 | + The western bbox edge. |
| 625 | + s : float |
| 626 | + The southern bbox edge. |
| 627 | + e : float |
| 628 | + The eastern bbox edge. |
| 629 | + n : float |
| 630 | + The northern bbox edge. |
| 631 | + provider_bounds : tuple of float |
| 632 | + Bounding values in cartesian coordinates: left, bottom, right, top |
| 633 | + |
| 634 | + Returns |
| 635 | + ------- |
| 636 | + zoom : int |
| 637 | + The zoom level to use in order to download this number of tiles. |
| 638 | + """ |
| 639 | + # Calculate bounds of the bbox |
| 640 | + lon_range = np.sort([e, w])[::-1] |
| 641 | + lat_range = np.sort([s, n])[::-1] |
| 642 | + |
| 643 | + lon_length = np.subtract(*lon_range) |
| 644 | + lat_length = np.subtract(*lat_range) |
| 645 | + |
| 646 | + left, bottom, right, top = provider_bounds |
| 647 | + |
| 648 | + # Calculate the zoom |
| 649 | + zoom_lon = np.ceil(np.log2((right - left) / lon_length)) |
| 650 | + zoom_lat = np.ceil(np.log2((top - bottom) / lat_length)) |
| 651 | + zoom = np.max([zoom_lon, zoom_lat]) |
| 652 | + return int(zoom) |
| 653 | + |
| 654 | + |
| 655 | +def _clamp_zoom_wmts(zoom, provider): |
| 656 | + msg = "Zoom level is outside of available levels. Fetching nearest available instead." |
| 657 | + if zoom < provider["min_zoom"]: |
| 658 | + warnings.warn(msg) |
| 659 | + return provider["min_zoom"] |
| 660 | + elif zoom > provider["max_zoom"]: |
| 661 | + warnings.warn(msg) |
| 662 | + return provider["max_zoom"] |
| 663 | + else: |
| 664 | + return zoom |
| 665 | + |
| 666 | + |
| 667 | +def bounds2img_wmts(left, bottom, right, top, url, zoom="auto", wait=0, max_retries=2): |
| 668 | + """ |
| 669 | + Arguments |
| 670 | + --------- |
| 671 | + left : float |
| 672 | + West edge |
| 673 | + bottom : float |
| 674 | + South edge |
| 675 | + right : float |
| 676 | + East edge |
| 677 | + top : float |
| 678 | + North edge |
| 679 | + url : contextily.TileProvider |
| 680 | + zoom : int |
| 681 | + Level of detail |
| 682 | +
|
| 683 | + Returns |
| 684 | + ------- |
| 685 | + img : ndarray |
| 686 | + Image as a 3D array of RGB values |
| 687 | + extent : tuple |
| 688 | + Bounding box [minX, maxX, minY, maxY] of the returned image |
| 689 | + """ |
| 690 | + (_left, _bottom), (_right, _top) = url["bounds"] |
| 691 | + provider_bounds = (_left, _bottom, _right, _top) |
| 692 | + if zoom == "auto": |
| 693 | + zoom = _calculate_zoom_wmts(left, bottom, right, top, provider_bounds) |
| 694 | + zoom = _clamp_zoom_wmts(zoom, url) |
| 695 | + tiles = [] |
| 696 | + arrays = [] |
| 697 | + for t in pt.tiles(left, bottom, right, top, [zoom], provider_bounds): |
| 698 | + x, y, z = t.x, t.y, t.z |
| 699 | + tile_url = _construct_tile_url(url, x, y, z) |
| 700 | + image = _fetch_tile(tile_url, wait, max_retries) |
| 701 | + tiles.append(t) |
| 702 | + arrays.append(image) |
| 703 | + merged, (left, bottom, right, top) = _merge_tiles_wmts(tiles, arrays) |
| 704 | + # Matplotlib expents them in a different order ... |
| 705 | + extent = (left, right, bottom, top) |
| 706 | + return merged, extent |
| 707 | + |
| 708 | + |
| 709 | +def bounds2raster_wmts( |
| 710 | + left, bottom, right, top, path, url, zoom="auto", wait=0, max_retries=2, |
| 711 | +): |
| 712 | + """ |
| 713 | + Arguments |
| 714 | + --------- |
| 715 | + left : float |
| 716 | + West edge |
| 717 | + bottom : float |
| 718 | + South edge |
| 719 | + right : float |
| 720 | + East edge |
| 721 | + top : float |
| 722 | + North edge |
| 723 | + path : str |
| 724 | + Path to raster file to be written |
| 725 | + url : contextily.TileProvider |
| 726 | + zoom : int |
| 727 | + Level of detail |
| 728 | +
|
| 729 | + Returns |
| 730 | + ------- |
| 731 | + img : ndarray |
| 732 | + Image as a 3D array of RGB values |
| 733 | + extent : tuple |
| 734 | + Bounding box [minX, maxX, minY, maxY] of the returned image |
| 735 | + """ |
| 736 | + # Download |
| 737 | + Z, ext = bounds2img_wmts(left, bottom, right, top, url, zoom, wait, max_retries) |
| 738 | + # Write |
| 739 | + # --- |
| 740 | + h, w, b = Z.shape |
| 741 | + # --- https://mapbox.github.io/rasterio/quickstart.html#opening-a-dataset-in-writing-mode |
| 742 | + minX, maxX, minY, maxY = ext |
| 743 | + x = np.linspace(minX, maxX, w) |
| 744 | + y = np.linspace(minY, maxY, h) |
| 745 | + resX = (x[-1] - x[0]) / w |
| 746 | + resY = (y[-1] - y[0]) / h |
| 747 | + transform = from_origin(x[0] - resX / 2, y[-1] + resY / 2, resX, resY) |
| 748 | + # --- |
| 749 | + with rio.open( |
| 750 | + path, |
| 751 | + "w", |
| 752 | + driver="GTiff", |
| 753 | + height=h, |
| 754 | + width=w, |
| 755 | + count=b, |
| 756 | + dtype=str(Z.dtype.name), |
| 757 | + transform=transform, |
| 758 | + ) as raster: |
| 759 | + for band in range(b): |
| 760 | + raster.write(Z[:, :, band], band + 1) |
| 761 | + return Z, ext |
0 commit comments