18

I have the seemingly tricky challenge of trying to work out a path, by sea, from one sea port to another sea port. The eventual aim is to plot this on a Google (or Bing) Map as a polyline.

The path needs to:

  • Be plausible, as a ship can't go over land (obviously)
  • Not run too close to the coast line. Ships can't go that far near to shore
  • Not be too complex. It's going to be plotted on Google Maps so a 2000 point polyline wont do.
  • Be the shortest, but not at the expense of the above three points

So, my first idea was obtain data on coast lines around the world. Such a thing is available here. Unfortunately it's incomplete however. OpenStreetMap shows this data and shorelines for things like Caribbean islands are missing.

I also thought about Geocoding (not reliable enough plus I would burn through thousands of requests trying to plot a route)

My next idea was to somehow use Google Maps and test if a point is blue or not. GMaps.NET, a great .NET Mapping component, allowed me to achieve this by creating a bitmap of what it renders and testing the color of a pixel.

First problem is that the accuracy of this hit testing is only as good as the resolution image of the image I test against. For ports close to each other, this is fine for ports further away, the accuracy suffers.

Second problem, assuming I use some kind of 'blue pixel testing' method, is what algorithm is right for finding a route. The A* algorithm looks promising, but I'm not sure how to push the path 'out' from the being to near to the coast. Nor how to reduce complexity of the polyline.

So ... any input: ideas, thoughts, links, sample code, etc would be welcome. Thanks.

(I should add that this is for a travel site. Accuracy isn't too important, I'm not directing shipping or anything)

geocodezip
  • 158,664
  • 13
  • 220
  • 245
NickH
  • 351
  • 5
  • 15
  • There's also http://www.openseamap.org , in case you weren't aware of it... – Bart Kiers Nov 14 '11 at 11:13
  • 3
    If you use a shortest path algorithm, such as A* or Dijkstra's shortest path, you can push the ships out from the shore by making the links between nodes longer than they are in real life for links between nodes that are near the coast. This would correspond to the captain taking the risk of the route into account as a factor, along with the fuel consumed and time taken. Note that if you model this as a graph route-finding problem the route may be influenced by the structure of the graph - see Manhattan distance. – mcdowella Nov 14 '11 at 18:27
  • Thanks for the openseamap.org link, but unfortunately it's based on the same incomplete shoreline data that OpenStreeMap uses. – NickH Nov 14 '11 at 20:43
  • A shame. Well, best of luck: sounds like a neat project! – Bart Kiers Nov 16 '11 at 08:51
  • Is this for real world use? Because if it is, then I think that you need to use a data source that is specifically approved for marine navigation. The data sources that you have listed will not have things that are critical IRL, such as shallows, sand bars, subsurface rocks, etc. that are not apparent from just a surface view/map. – RBarryYoung Jun 21 '18 at 17:52
  • What constraints do you have for language or tools? A GIS would be convenient for this, ArcGIS or GRASS for example; R has some easy packages to do this kind of thing as well. – J. Win. Jun 24 '18 at 18:33
  • contraints : R language and leaftet for polylines. – Wilcar Jun 25 '18 at 11:17
  • Do you talk about gdistance package ? – Wilcar Jun 25 '18 at 11:19

5 Answers5

4

Here's a solution using R. Could be much refined, simply by making certain regions expensive or cheap for the shortest path algorithm. For example exclude the Arctic Ocean, allow major rivers/canals, make known shipping routes preferred for travel.

library(raster)
library(gdistance)
library(maptools)
library(rgdal)

# make a raster of the world, low resolution for simplicity
# with all land having "NA" value
# use your own shapefile or raster if you have it
# the wrld_simpl data set is from maptools package
data(wrld_simpl)

# a typical world projection
world_crs <- crs(wrld_simpl)
world <- wrld_simpl
worldshp <- spTransform(world, world_crs)
ras <- raster(nrow=300, ncol=300)

# rasterize will set ocean to NA so we just inverse it
# and set water to "1"
# land is equal to zero because it is "NOT" NA
worldmask <- rasterize(worldshp, ras)
worldras <- is.na(worldmask)

# originally I set land to "NA"
# but that would make some ports impossible to visit
# so at 999 land (ie everything that was zero)
# becomes very expensive to cross but not "impossible" 
worldras[worldras==0] <- 999
# each cell antras now has value of zero or 999, nothing else

# create a Transition object from the raster
# this calculation took a bit of time
tr <- transition(worldras, function(x) 1/mean(x), 16)
tr = geoCorrection(tr, scl=FALSE)

# distance matrix excluding the land, must be calculated
# from a point of origin, specified in the CRS of your raster
# let's start with latlong in Black Sea as a challenging route
port_origin <- structure(c(30, 40), .Dim = 1:2)
port_origin <- project(port_origin, crs(world_crs, asText = TRUE))
points(port_origin)

# function accCost uses the transition object and point of origin
A <- accCost(tr, port_origin)

# now A still shows the expensive travel over land
# so we mask it out to display sea travel only
A <- mask(A, worldmask, inverse=TRUE)

# and calculation of a travel path, let's say to South Africa
port_destination <- structure(c(20, -35), .Dim = 1:2)
port_destination <- project(port_destination, crs(world_crs, asText = TRUE))

path <- shortestPath(tr, port_origin, port_destination, output = "SpatialLines")
 
# make a demonstration plot
plot(A)
points(rbind(port_origin, port_destination))
lines(path)

# we can wrap all this in a customized function
# if you two points in the projected coordinate system,
# and a raster whose cells are weighted 
# according to ease of shipping
RouteShip <- function(from_port, to_port, cost_raster, land_mask) {
  tr <- transition(cost_raster, function(x) 1/mean(x), 16)
  tr = geoCorrection(tr, scl=FALSE)
  A <- accCost(tr, from_port)
  A <- mask(A, land_mask, inverse=TRUE)
  path <- shortestPath(tr, from_port, to_port, output = "SpatialLines")
 
  plot(A)
  points(rbind(from_port, to_port))
  lines(path)
}

RouteShip(port_origin, port_destination, worldras, worldmask)

enter image description here

J. Win.
  • 6,662
  • 7
  • 34
  • 52
  • A great answer. Unfortunately I can't reproduce your script on my computer.(portA over port B, no path) – Wilcar Jun 25 '18 at 19:53
  • I'm not sure what portA and port B are. Did you get a particular error message from the R code? – J. Win. Jun 26 '18 at 07:37
  • I have 2 dots overlapping at 0,0 x,y coord – Wilcar Jun 26 '18 at 07:38
  • A possible explanation would be the points are in degrees lat/long, and the rest of the map is projected in a meters-based system. Without looking at the data items that you have, it's hard to diagnose. – J. Win. Jun 26 '18 at 13:59
  • Than you for helping. I just made a copy / paste of your script. – Wilcar Jun 26 '18 at 14:03
  • Thanks for this - But how to output the distance of the shortest path between these points in e.g. 1,283 km? Is this distance contained in the 'paths' object? I would like to calculate the distance between the two points based on the shortest sea route... – user95146 Mar 08 '21 at 17:21
  • Hey. Can explain how you got to `tr <- transition(cost_raster, function(x) 1/mean(x), 16)`? – Jon Mar 10 '21 at 06:20
  • https://www.rdocumentation.org/packages/gdistance/versions/1.3-6/topics/transition – J. Win. Mar 11 '21 at 08:31
  • It's been awhile but I know 16 looks at the nearest sixteen raster cells out to a chess knight move. 1/mean(x) was how I defined the cost function for traveling through a particular raster cell, can't remember why – J. Win. Mar 11 '21 at 08:34
  • From this script how to calculate the distance in km or nautical miles between port_origin and port_destination using the shortest path found? – Bala Jun 24 '21 at 04:02
  • @Bala that's probably a new question to post. If you do so let me know. – J. Win. Dec 21 '21 at 17:22
3

To simplify the polyline you get from e.g. A* search, you can use an algorithm like Douglas-Peucker. See also this list of references: http://maven.smith.edu/~orourke/TOPP/P24.html.

Alternative idea: The usual way to apply A* would be to consider each pixel as a possible state (position), but there's no reason why you couldn't use just a subset of the pixels as possible states instead. If you make the density of states near the beginning and endpoints high, and the density of states far from either endpoint low, then you'll automatically get paths that begin and end with short, precise movements, but have long straight segments in the middle (e.g. when crossing the Pacific). If you do this, you might want to also increase the density of positions near land.

Another possible A* tweak: You can incorporate "current direction" into the state, and penalise movements that cause a change in direction. This will tend to produce long straight lines in your path. This will multiply your state space by 8, but that's probably bearable. Because you're only adding to the cost of a solution, the straight-line-to-destination heuristic you would normally use remains admissible for this new cost function, so no complications arise there.

j_random_hacker
  • 50,331
  • 10
  • 105
  • 169
  • Thanks for the link and the ideas. Both A* tweaks make sense and seem worth investigating – NickH Nov 14 '11 at 14:17
  • You're welcome! I think the 1st tweak could be more trouble than it's worth though (unless you find memory is tight) because it makes it more tricky to discover which neighbouring states any state has, and because you'll need to check that a straight-line path between 2 non-adjacent states doesn't intersect any land. – j_random_hacker Nov 14 '11 at 14:53
2

Second problem, assuming I use some kind of 'blue pixel testing' method, is what algorithm is right for finding a route. The A* algorithm looks promising, but I'm not sure how to push the path 'out' from the being to near to the coast. Nor how to reduce complexity of the polyline.

First create the binary sea image of the world (white: is sea, black: not sea), then erode the image. All white points after erosion are navigable. Neglecting the odd sandbank or two, of course.

As you might guess, this approach reveals a central problem in your pathfinding: Most ships have to steer quite close to land to reach a port, violating the navigation rules. However, that could be solved by starting navigation at the closest navigable sea point adjacent to a given harbour.

thiton
  • 35,651
  • 4
  • 70
  • 100
  • Thanks thiton, I like the idea of a 'sea point' adjacent from the harbour to push the route out from the coast. – NickH Nov 14 '11 at 11:20
  • I have resolved such a problem by making land pixels very expensive to cross, but not impossible. For example, if travelling a km of open sea adds one to the movement path, travelling a km of land might add 10^4. That way, when you have to cross "land" to get in or out of port, you will do it, but the rest of the time, you will not. Something like the Suez or Panama Canal you should double check to be sure is handled reasonably, as it does save thousands of km. – J. Win. Jun 24 '18 at 12:33
0

If I were you I would choose a graph theory approach. Your only problem would be to gather the edges of the database. If you have them you can use A* or Dijkstra's algo to plan the shortest route. Anyways, if I assume it right you need something similar to (Searoutefinder) right? Good luck!

EZMapdesign
  • 203
  • 1
  • 5
0

Here is a Python approach using scgraph. In theory, you can use scgraph to solve shortest path for various network graphs using some modified Dijkstra algorithms designed to be very efficient for sparse graphs.

The package comes with some basic network graphs to get you started. There are a couple of maritime network graphs to use.

Here is an example from the docs using a slightly modified (adjusted for anti meridian support) Marnet data:

# Use a maritime network geograph
from scgraph.geographs.marnet import marnet_geograph

# Get the shortest path between 
output = marnet_geograph.get_shortest_path(
    origin_node={"latitude": 31.23,"longitude": 121.47}, 
    destination_node={"latitude": 32.08,"longitude": -81.09}
)
print('Length: ',output['length']) #=> Length:  19596.4653
print(str([[i['latitude'],i['longitude']] for i in output['coordinate_path']])) #=> [[...],...]

Depending on your graphing needs you would need to format the coordinate path accordingly for the gis system you are using.

This would produce a route like:

A map of the coordinate points from Hong Kong to Savannah

Note: The length produced by scgraph assumes that all non network based geograph travel has a circuity factor of 3. This is used as a way to force the optimization algorithm to choose a reasonably close network node to the provided coordinates that might not be in the network. Assuming you use an actual seaport with maritime network data, this effect should be marginal. If however you accidentally choose a port located in the middle of a landlocked area, that first leg / last leg of the journey will add 3x the great circle distance traveled.

As a side note, you might want to simplify the end route to reduce data overhead. A good option for reducing data overhead is mapshaper (web, cli).

conmak
  • 1,200
  • 10
  • 13