Skip to content

Commit

Permalink
Add neighbors feature, closes #118 (#123)
Browse files Browse the repository at this point in the history
* Adds neighbors feature, closes #118

* Fixes documentation for children command

* Fixes duplicate line strip() in children command

* Enables Python 3.8 and 3.9 in tox

* Enables Python 3.9 environment on Travis

* Adds a dockerfile for development
  • Loading branch information
daniel-j-h authored Apr 8, 2021
1 parent d00e293 commit b4ccb95
Show file tree
Hide file tree
Showing 9 changed files with 169 additions and 3 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ python:
- "3.6"
- "3.7"
- "3.8"
- "3.9"
install:
- "pip install -r requirements.txt"
- "pip install pytest-cov~=2.8 pytest~=5.3.0"
Expand Down
5 changes: 5 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
Changes
=======

1.1.7 (2021-03-01)
------------------

- Add ``neighbors`` function and command to get adjacent tiles (#118).

1.1.6 (2020-08-24)
------------------

Expand Down
24 changes: 24 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# To develop in a reproducible dev environment
#
# 1. build the docker image
#
# docker build -t mapbox/mercantile .
#
# 2. mount the source into the container and run tests
#
# docker run --rm -v $PWD:/usr/src/app mapbox/mercantile


FROM python:3.9-slim

WORKDIR /usr/src/app

COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
RUN pip install pytest-cov~=2.8 pytest~=5.3.0

COPY . .

RUN pip install -e .[test]

CMD ["python", "-m", "pytest"]
1 change: 1 addition & 0 deletions docs/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Command line interface
bounding-tile Print the bounding tile of a lng/lat point, bounding box, or
GeoJSON objects.
children Print the children of the tile.
neighbors Print the neighbors of the tile.
parent Print the parent tile.
quadkey Convert to/from quadkeys.
shapes Print the shapes of tiles as GeoJSON.
Expand Down
52 changes: 52 additions & 0 deletions mercantile/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"children",
"feature",
"lnglat",
"neighbors"
"parent",
"quadkey",
"quadkey_to_tile",
Expand Down Expand Up @@ -264,6 +265,57 @@ def lnglat(x, y, truncate=False):
return LngLat(lng, lat)


def neighbors(*tile, **kwargs):
"""Get the neighbors of a tile
The neighbors function makes no guarantees regarding neighbor tile ordering.
The neighbors function returns up to eight neighboring tiles, where tiles
will be omitted when they are not valid e.g. Tile(-1, -1, z).
Parameters
----------
tile : Tile or sequence of int
May be be either an instance of Tile or 3 ints, X, Y, Z.
Returns
-------
list
Examples
--------
>>> neighbors(Tile(486, 332, 10))
[Tile(x=485, y=331, z=10), Tile(x=485, y=332, z=10), Tile(x=485, y=333, z=10), Tile(x=486, y=331, z=10), Tile(x=486, y=333, z=10), Tile(x=487, y=331, z=10), Tile(x=487, y=332, z=10), Tile(x=487, y=333, z=10)]
"""
tile = _parse_tile_arg(*tile)

xtile, ytile, ztile = tile

tiles = []

for i in [-1, 0, 1]:
for j in [-1, 0, 1]:
if i == 0 and j == 0:
continue

tiles.append(Tile(x=xtile + i, y=ytile + j, z=ztile))

# Make sure to not generate invalid tiles for valid input
# https://github.com/mapbox/mercantile/issues/122
def valid(tile):
validx = 0 <= tile.x <= 2 ** tile.z - 1
validy = 0 <= tile.y <= 2 ** tile.z - 1
validz = 0 <= tile.z
return validx and validy and validz

tiles = [t for t in tiles if valid(t)]

return tiles


def xy_bounds(*tile):
"""Get the web mercator bounding box of a tile
Expand Down
39 changes: 37 additions & 2 deletions mercantile/scripts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -457,15 +457,14 @@ def children(ctx, input, depth):
https://tools.ietf.org/html/rfc8142 and
https://tools.ietf.org/html/rfc7159).
$ echo "[486, 332, 10]" | mercantile parent
$ echo "[486, 332, 10]" | mercantile children
Output:
[243, 166, 9]
"""
src = normalize_input(input)
for line in iter_lines(src):
line = line.strip()
tiles = [json.loads(line)[:3]]
for i in range(depth):
tiles = sum([mercantile.children(t) for t in tiles], [])
Expand Down Expand Up @@ -509,6 +508,42 @@ def parent(ctx, input, depth):
output = json.dumps(tile)
click.echo(output)

# The neighbors command.
@cli.command(short_help="Print the neighbors of the tile.")
@click.argument("input", default="-", required=False)
@click.pass_context
def neighbors(ctx, input):
"""Takes [x, y, z] tiles as input and writes adjacent
tiles on the same zoom level to stdout in the same form.
There are no ordering guarantees for the output tiles.
Input may be a compact newline-delimited sequences of JSON or
a pretty-printed ASCII RS-delimited sequence of JSON (like
https://tools.ietf.org/html/rfc8142 and
https://tools.ietf.org/html/rfc7159).
$ echo "[486, 332, 10]" | mercantile neighbors
Output:
[485, 331, 10]
[485, 332, 10]
[485, 333, 10]
[486, 331, 10]
[486, 333, 10]
[487, 331, 10]
[487, 332, 10]
[487, 333, 10]
"""
src = normalize_input(input)
for line in iter_lines(src):
tile = json.loads(line)[:3]
tiles = mercantile.neighbors(tile)
for t in tiles:
output = json.dumps(t)
click.echo(output)


@cli.command(short_help="Convert to/from quadkeys.")
@click.argument("input", default="-", required=False)
Expand Down
26 changes: 26 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Tests of the mercantile CLI"""

import json

from click.testing import CliRunner
import pytest

Expand Down Expand Up @@ -284,6 +286,30 @@ def test_cli_children():
)


def test_cli_neighbors():
runner = CliRunner()
result = runner.invoke(cli, ["neighbors"], "[243, 166, 9]")
assert result.exit_code == 0

tiles = result.output.strip().split("\n")
tiles = [json.loads(t) for t in tiles]
assert len(tiles) == 8

# We do not provide ordering guarantees
tiles = set([tuple(t) for t in tiles])

assert (243, 166, 9) not in tiles, "input not in neighbors"

assert (243 - 1, 166 - 1, 9) in tiles
assert (243 - 1, 166 + 0, 9) in tiles
assert (243 - 1, 166 + 1, 9) in tiles
assert (243 + 0, 166 - 1, 9) in tiles
assert (243 + 0, 166 + 1, 9) in tiles
assert (243 + 1, 166 - 1, 9) in tiles
assert (243 + 1, 166 + 0, 9) in tiles
assert (243 + 1, 166 + 1, 9) in tiles


def test_cli_strict_overlap_contain():
runner = CliRunner()
result1 = runner.invoke(cli, ["shapes"], "[2331,1185,12]")
Expand Down
22 changes: 22 additions & 0 deletions tests/test_funcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,28 @@ def test_parent_bad_tile_zoom():
assert "zoom must be an integer and less than" in str(e.value)


def test_neighbors():
x, y, z = 243, 166, 9
tiles = mercantile.neighbors(x, y, z)
assert len(tiles) == 8
assert all(t.z == z for t in tiles)
assert all(t.x - x in (-1, 0, 1) for t in tiles)
assert all(t.y - y in (-1, 0, 1) for t in tiles)

def test_neighbors_invalid():
x, y, z = 0, 166, 9
tiles = mercantile.neighbors(x, y, z)
assert len(tiles) == 8 - 3 # no top-left, left, bottom-left
assert all(t.z == z for t in tiles)
assert all(t.x - x in (-1, 0, 1) for t in tiles)
assert all(t.y - y in (-1, 0, 1) for t in tiles)

def test_neighbors_invalid():
x, y, z = 0, 0, 0
tiles = mercantile.neighbors(x, y, z)
assert len(tiles) == 0 # root tile has no neighbors


def test_simplify():
children = mercantile.children(243, 166, 9, zoom=12)
assert len(children) == 64
Expand Down
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tox]
envlist =
py27,py34,py35,py36,py37
py27,py34,py35,py36,py37,py38,py39

[testenv]
deps =
Expand Down

0 comments on commit b4ccb95

Please sign in to comment.