import_new_meshes(self)

Look for new meshes and insert them into the database.

Source code in polarrouteserver/route_api/tasks.py
@app.task(bind=True)
def import_new_meshes(self):
    """Look for new meshes and insert them into the database."""

    if settings.MESH_METADATA_DIR is None:
        raise ValueError("MESH_METADATA_DIR has not been set.")

    # find the latest metadata file
    files = os.listdir(settings.MESH_METADATA_DIR)
    file_list = [
        os.path.join(settings.MESH_METADATA_DIR, file)
        for file in files
        if file.startswith("upload_metadata_") and file.endswith(".yaml.gz")
    ]
    if len(file_list) == 0:
        msg = "Upload metadata file not found."
        logger.error(msg)
        return
    latest_metadata_file = max(file_list, key=os.path.getctime)

    # load in the metadata
    logger.info(
        f"Loading metadata file from {os.path.join(settings.MESH_METADATA_DIR, latest_metadata_file)}"
    )
    with gzip.open(latest_metadata_file, "rb") as f:
        metadata = yaml.load(f.read(), Loader=yaml.Loader)

    meshes_added = []
    for record in metadata["records"]:
        # we only want the vessel json files
        if not bool(re.search(VESSEL_MESH_FILENAME_PATTERN, record["filepath"])):
            continue

        # extract the filename from the filepath
        mesh_filename = record["filepath"].split("/")[-1]

        # load in the mesh json
        try:
            zipped_filename = mesh_filename + ".gz"
            with gzip.open(
                Path(settings.MESH_DIR, zipped_filename), "rb"
            ) as gzipped_mesh:
                mesh_json = json.load(gzipped_mesh)
        except FileNotFoundError:
            logger.warning(f"{zipped_filename} not found. Skipping.")
            continue
        except PermissionError:
            logger.warning(
                f"Can't read {zipped_filename} due to permission error. File may still be transferring. Skipping."
            )
            continue

        # write out the unzipped mesh to temp file
        tfile = tempfile.NamedTemporaryFile(mode="w+", delete=True)
        json.dump(mesh_json, tfile, indent=4)
        tfile.flush()
        md5 = calculate_md5(tfile.name)

        # cross reference md5 hash from file record in metadata to actual file on disk
        if md5 != record["md5"]:
            logger.warning(
                f"Mesh file md5: {md5}\n\
                           does not match\n\
                           Metadata md5: {record['md5']}\n\
                           Skipping."
            )
            # if md5 hash from metadata file does not match that of the file itself,
            # there may have been a filename clash, skip this one.
            continue

        # create an entry in the database
        mesh, created = Mesh.objects.get_or_create(
            md5=md5,
            defaults={
                "name": mesh_filename,
                "valid_date_start": datetime.datetime.strptime(
                    mesh_json["config"]["mesh_info"]["region"]["start_time"], "%Y-%m-%d"
                ).replace(tzinfo=datetime.timezone.utc),
                "valid_date_end": datetime.datetime.strptime(
                    mesh_json["config"]["mesh_info"]["region"]["end_time"], "%Y-%m-%d"
                ).replace(tzinfo=datetime.timezone.utc),
                "created": datetime.datetime.strptime(
                    record["created"], "%Y%m%dT%H%M%S"
                ).replace(tzinfo=datetime.timezone.utc),
                "json": mesh_json,
                "meshiphi_version": record["meshiphi"],
                "lat_min": record["latlong"]["latmin"],
                "lat_max": record["latlong"]["latmax"],
                "lon_min": record["latlong"]["lonmin"],
                "lon_max": record["latlong"]["lonmax"],
            },
        )
        if created:
            logger.info(
                f"Adding new mesh to database: {mesh.id} {mesh.name} {mesh.created}"
            )
            meshes_added.append(
                {"id": mesh.id, "md5": record["md5"], "name": mesh.name}
            )

    return meshes_added

optimise_route(self, route_id, backup_mesh_ids=None)

Use PolarRoute to calculate optimal route from Route database object and mesh. Saves Route in database and returns route geojson as dictionary.

Parameters:
  • route_id (int) –

    id of record in Route database table

  • backup_mesh_ids (list, default: None ) –

    list of database ids of backup meshes to try in order of priority

Returns:
  • dict

    route geojson as dictionary

Source code in polarrouteserver/route_api/tasks.py
@app.task(bind=True)
def optimise_route(
    self,
    route_id: int,
    backup_mesh_ids: list[int] = None,
) -> dict:
    """
    Use PolarRoute to calculate optimal route from Route database object and mesh.
    Saves Route in database and returns route geojson as dictionary.

    Params:
        route_id: id of record in Route database table
        backup_mesh_ids list: list of database ids of backup meshes to try in order of priority

    Returns:
        route geojson as dictionary
    """
    route = Route.objects.get(id=route_id)
    mesh = route.mesh
    logger.info(f"Running optimisation for route {route.id}")
    logger.info(f"Using mesh {mesh.id}")
    if backup_mesh_ids:
        logger.info(f"Also got backup mesh ids {backup_mesh_ids}")

    # add warning on mesh date if older than today
    if mesh.created.date() < datetime.datetime.now().date():
        route.info = {
            "info": f"Latest available mesh from {datetime.datetime.strftime(mesh.created, '%Y/%m/%d %H:%M%S')}"
        }

    data_warning_message = check_mesh_data(mesh)
    if data_warning_message != "":
        if route.info is None:
            route.info = {"info": data_warning_message}
        else:
            route.info["info"] = route.info["info"] + data_warning_message

    # convert waypoints into pandas dataframe for PolarRoute
    waypoints = pd.DataFrame(
        {
            "Name": [
                "Start" if route.start_name is None else route.start_name,
                "End" if route.end_name is None else route.end_name,
            ],
            "Lat": [route.start_lat, route.end_lat],
            "Long": [route.start_lon, route.end_lon],
            "Source": ["X", np.nan],
            "Destination": [np.nan, "X"],
        }
    )

    try:
        unsmoothed_routes = []
        route_planners = []
        configs = (
            settings.TRAVELTIME_CONFIG,
            settings.FUEL_CONFIG,
        )
        for config in configs:
            rp = RoutePlanner(copy.deepcopy(mesh.json), config)

            # Calculate optimal dijkstra path between waypoints
            rp.compute_routes(waypoints)

            route_planners.append(rp)

            # save the initial unsmoothed route
            logger.info(
                f"Saving unsmoothed Dijkstra paths for {config['objective_function']}-optimised route."
            )
            if len(rp.routes_dijkstra) == 0:
                raise ValueError("Inaccessible. No routes found.")
            route_geojson = extract_geojson_routes(rp.to_json())
            route_geojson[0]["features"][0]["properties"]["objective_function"] = (
                config["objective_function"]
            )
            unsmoothed_routes.append(route_geojson)
            route.json_unsmoothed = unsmoothed_routes
            route.calculated = timezone.now()
            route.polar_route_version = polar_route.__version__
            route.save()

        smoothed_routes = []
        for i, rp in enumerate(route_planners):
            # Smooth the dijkstra routes
            rp.compute_smoothed_routes()
            # Save the smoothed route(s)
            logger.info(f"Route smoothing {i + 1}/{len(route_planners)} complete.")
            route_geojson = extract_geojson_routes(rp.to_json())
            route_geojson[0]["features"][0]["properties"]["objective_function"] = (
                rp.config["objective_function"]
            )
            smoothed_routes.append(route_geojson)

            # Update the database
            route.json = smoothed_routes
            route.calculated = timezone.now()
            route.polar_route_version = polar_route.__version__
            route.save()

        return smoothed_routes

    except Exception as e:
        logger.error(e)
        self.update_state(state=states.FAILURE)
        # this is awful, polar route should raise a custom error class
        if "Inaccessible. No routes found" in e.args[0] and len(backup_mesh_ids) > 0:
            # if route is inaccesible in the mesh, try again if backup meshes are provided
            logger.info(
                f"No routes found on mesh {mesh.id}, trying with next mesh(es) {backup_mesh_ids}"
            )
            route.info = {"info": "Route inaccessible on mesh, trying next mesh."}
            route.mesh = Mesh.objects.get(id=backup_mesh_ids[0])
            route.save()
            task = optimise_route.delay(route.id, backup_mesh_ids[1:])
            _ = Job.objects.create(
                id=task.id,
                route=route,
            )
            raise Ignore()
        else:
            route.info = {"error": f"{e}"}
            route.save()
            raise Ignore()