def make_grid_for_reconstruction_time(raw_point_file,
                                      age_grid_time,
                                      grdspace,
                                      region,
                                      output_dir,
                                      output_filename_template='seafloor_age_',
                                      GridColumnFlag=2):
    # given a set of reconstructed points with ages, makes a global grid using
    # blockmedian and sphinterpolate

    block_median_points = tempfile.NamedTemporaryFile(delete=False)
    block_median_points.close(
    )  # Cannot open twice on Windows - close before opening again.

    call_system_command([
        'gmt', 'blockmedian', raw_point_file, '-I{0}d'.format(grdspace),
        '-R{0}'.format(region), '-V', '-i0,1,{0}'.format(GridColumnFlag), '>',
        block_median_points.name
    ])

    call_system_command([
        'gmt', 'sphinterpolate', block_median_points.name,
        '-G{0}/unmasked/{1}{2}Ma.nc'.format(output_dir,
                                            output_filename_template,
                                            age_grid_time),
        '-I{0}d'.format(grdspace), '-R{0}'.format(region), '-V'
    ])

    os.unlink(block_median_points.name
              )  # Remove temp file (because we set 'delete=False').
def mask_synthetic_points(reconstructed_present_day_lons,
                          reconstructed_present_day_lats,
                          reconstructed_present_day_ages,
                          raw_point_file,
                          grdspace,
                          region,
                          buffer_distance_degrees=1):
    # given an existing ascii file contain only synthetic points at a given reconstruction time,
    # and arrays defining points from the reconstructed present day age grid at this same time,
    # --> create a mask to remove the synthetic points in the area of overlap (+ some buffer)
    # --> apply the mask to remove the overlapping points
    # --> concatentate the remaining synthetic points with the reconstructed present-day age grid points

    reconstructed_present_day_age_file = tempfile.NamedTemporaryFile(
        delete=False)
    synthetic_age_masked_file = tempfile.NamedTemporaryFile(delete=False)
    masking_grid_file = tempfile.NamedTemporaryFile(delete=False)

    # Cannot open twice on Windows - close before opening again.
    reconstructed_present_day_age_file.close()
    synthetic_age_masked_file.close()
    masking_grid_file.close()

    write_xyz_file(
        reconstructed_present_day_age_file.name,
        zip(reconstructed_present_day_lons, reconstructed_present_day_lats,
            reconstructed_present_day_ages))

    call_system_command([
        'gmt', 'grdmask', reconstructed_present_day_age_file.name,
        '-G%s' % masking_grid_file.name, '-I{0}d'.format(grdspace),
        '-R{0}'.format(region),
        '-S%0.6fd' % buffer_distance_degrees, '-V'
    ])

    call_system_command([
        'gmt', 'gmtselect', raw_point_file.name,
        '-G%s' % masking_grid_file.name, '-Ig', '>',
        synthetic_age_masked_file.name
    ])

    # concatenate the files in a cross-platform way - overwriting the file that contains unmasked synthetic points
    with open(raw_point_file.name, 'w') as outfile:
        for fname in [
                synthetic_age_masked_file.name,
                reconstructed_present_day_age_file.name
        ]:
            with open(fname) as infile:
                for line in infile:
                    outfile.write(line)

    # Remove temp file (because we set 'delete=False').
    os.unlink(reconstructed_present_day_age_file.name)
    os.unlink(synthetic_age_masked_file.name)
    os.unlink(masking_grid_file.name)
def grdcontour2feature(grdfile,clevel,return_polygons=True):

    # call GMT to get a single contour at the specified value of clevel
    call_system_command(['gmt',
                         'grdcontour',
                         grdfile,
                         '-Rd',
                         '-C+%0.8f' % clevel,
                         '-S4',
                         '-Dcontour_%c.txt'])

    # read in the GMT delimited xyz ascii file, 
    # create a list of lists with polygon coordinates
    f = open('./contour_C.txt', 'r')

    polygons = []
    contourlist = []
    for line in f:
        if line[0] == '>':
            if len(contourlist)>0:
                polygons.append(contourlist)
            contourlist = []
        else:
            line = line.split()
            contourlist.append([float(j) for j in line])
            #break

    # create gplates-format features
    polyline_features = []
    for p in polygons:
        pf = pygplates.PolylineOnSphere(zip(list(zip(*p))[1],list(zip(*p))[0]))
        polyline_features.append(pf)

    # use join to handle polylines split across dateline
    joined_polyline_features = pygplates.PolylineOnSphere.join(polyline_features)

    if return_polygons:
    # force polylines to be polygons
        joined_polygon_features = []
        for geom in joined_polyline_features:
            polygon = pygplates.Feature()
            polygon.set_geometry(pygplates.PolygonOnSphere(geom))
            joined_polygon_features.append(polygon)
            
        return joined_polygon_features

    else:
        return joined_polyline_features
def paleotopography_job(reconstruction_time,
                        paleogeography_timeslice_list,
                        tween_basedir,
                        reconstruction_basedir,
                        output_dir,
                        file_format,
                        rotation_file,
                        COBterrane_file,
                        agegrid_file_template,
                        lowland_elevation,
                        shallow_marine_elevation,
                        max_mountain_elevation,
                        depth_for_unknown_ocean,
                        sampling,
                        mountain_buffer_distance_degrees,
                        area_threshold,
                        grid_smoothing_wavelength_kms,
                        merge_with_bathymetry,
                        land_or_ocean_precedence='land',
                        netcdf3_output=False,
                        subdivision_depth=4):

    print('Working on Time %0.2fMa\n' % reconstruction_time)

    rotation_model = pygplates.RotationModel(rotation_file)

    # find times that bracket the selected exact time in the paleogeography source files
    time_stage_max = paleogeography_timeslice_list[np.where(
        paleogeography_timeslice_list > reconstruction_time)[0][0]]
    time_stage_min = paleogeography_timeslice_list[np.where(
        paleogeography_timeslice_list <= reconstruction_time)[0][-1]]

    # Note the logic for selecting the times:
    # The main issue is that each set of paleogeography polygons has a defined 'midpoint' time
    # --> if the reconstruction time is between these, the choice of t1 and t2 is obvious
    # --> if the reconstruction time matches one of these times, then we can work directly on
    #     the geometries that match this time - hence the two routes through the if statement below

    print('Selected Time is in the stage %0.2fMa to %0.2fMa' %
          (time_stage_min, time_stage_max))

    land_points_file = '%s/tweentest_land_%0.2fMa_%0.2fMa.%s' % (
        tween_basedir, time_stage_min, time_stage_max, file_format)
    marine_points_file = '%s/tweentest_ocean_%0.2fMa_%0.2fMa.%s' % (
        tween_basedir, time_stage_min, time_stage_max, file_format)
    mountains_going_up_file = '%s/mountain_transgression_%0.2fMa_%0.2fMa.%s' % (
        tween_basedir, time_stage_min, time_stage_max, file_format)
    mountains_going_down_file = '%s/mountain_regression_%0.2fMa_%0.2fMa.%s' % (
        tween_basedir, time_stage_min, time_stage_max, file_format)
    mountains_stable_file = '%s/mountain_stable_%0.2fMa_%0.2fMa.%s' % (
        tween_basedir, time_stage_min, time_stage_max, file_format)

    # get a nx3 array defining the points above sea-level, reconstructed to time of interest
    # columns are [lat, long, elevation assigned for lowland]
    land_point_array = add_reconstructed_points_to_xyz(land_points_file,
                                                       rotation_model,
                                                       reconstruction_time,
                                                       lowland_elevation)

    # get a nx3 array defining shallow marine areas, reconstructed to time of interest
    # columns are [lat, long, elevation assigned for shallow marine]
    marine_point_array = add_reconstructed_points_to_xyz(
        marine_points_file, rotation_model, reconstruction_time,
        shallow_marine_elevation)

    # Note that the two arrays just created are based on 'regular' lat/long grids, but
    # are not aligned with the regular lat/long grid that we want to output
    # since they are (usually) reconstructed to a different time from the one at which they
    # were created (and anyway may be at a different resolution to the grid sampling specified
    # here)

    # combine the previous two arrays
    pg_point_array = np.vstack((land_point_array, marine_point_array))

    # get a merged version of COB terranes, optionally excluding polygons that are small in area
    # TODO deal with donut polygons better
    sieve_polygons = get_merged_cob_terrane_polygons(COBterrane_file,
                                                     rotation_model,
                                                     reconstruction_time,
                                                     sampling)

    # get arrays defining the land and sea based on which points fall within the COB terranes
    # NOTE this step is where we create the points that ARE on the regular lat/long grid we
    # will ultimately output
    (lat, lon, zval, lat_deep, lon_deep,
     zval_deep) = get_land_sea_multipoints(sieve_polygons,
                                           sampling,
                                           depth_for_unknown_ocean,
                                           subdivision_depth=subdivision_depth)

    # sample the land/marine points onto the points within the COB Terranes
    # This will fill the gaps that exist within continents, and average out overlaps
    d, l = sampleOnSphere(pg_point_array[:, 1],
                          pg_point_array[:, 0],
                          pg_point_array[:, 2],
                          np.array(lon),
                          np.array(lat),
                          n=1)

    land_marine_interp_points = pg_point_array[:, 2].ravel()[l]

    # At this point, the land points are all considered to be 'lowland'......

    ####################################
    # Deal with the mountains
    if np.equal(reconstruction_time, time_stage_min):
        print('Temporary fix for valid time')
        #dat3 = add_reconstructed_points_to_xyz(mountains_going_up_file,rotation_model,reconstruction_time,3)
        dat4 = add_reconstructed_points_to_xyz(mountains_going_down_file,
                                               rotation_model,
                                               reconstruction_time + 0.01, 1)
        dat5 = add_reconstructed_points_to_xyz(mountains_stable_file,
                                               rotation_model,
                                               reconstruction_time + 0.01, 1)
        mountains_tr_point_array = np.vstack((dat4, dat5))

        dist_tr = get_distance_to_mountain_edge(mountains_tr_point_array,
                                                reconstruction_basedir,
                                                rotation_model,
                                                reconstruction_time,
                                                area_threshold)
        dist_tr_cap = np.array(dist_tr)
        dist_tr_cap[
            np.array(dist_tr) >
            mountain_buffer_distance_degrees] = mountain_buffer_distance_degrees

        dist_tr_cap = dist_tr_cap * mountains_tr_point_array[:, 2]

        normalized_mountain_elevation = dist_tr_cap

    else:
        # load in the mountain points but at three different times: t1 and t2, and the reconstruction time
        # note that these three arrays should all be identical in size, since they are the same multipoints
        # just reconstructed to three slightly different times
        dat3 = add_reconstructed_points_to_xyz(mountains_going_up_file,
                                               rotation_model, time_stage_max,
                                               1)
        dat4 = add_reconstructed_points_to_xyz(mountains_going_down_file,
                                               rotation_model, time_stage_max,
                                               1)
        dat5 = add_reconstructed_points_to_xyz(mountains_stable_file,
                                               rotation_model, time_stage_max,
                                               1)
        mountains_t2_point_array = np.vstack((dat3, dat4, dat5))

        dat3 = add_reconstructed_points_to_xyz(mountains_going_up_file,
                                               rotation_model,
                                               time_stage_min + 0.01, 1)
        dat4 = add_reconstructed_points_to_xyz(mountains_going_down_file,
                                               rotation_model,
                                               time_stage_min + 0.01, 1)
        dat5 = add_reconstructed_points_to_xyz(mountains_stable_file,
                                               rotation_model,
                                               time_stage_min + 0.01, 1)
        mountains_t1_point_array = np.vstack((dat3, dat4, dat5))

        dat3 = add_reconstructed_points_to_xyz(mountains_going_up_file,
                                               rotation_model,
                                               reconstruction_time, 1)
        dat4 = add_reconstructed_points_to_xyz(mountains_going_down_file,
                                               rotation_model,
                                               reconstruction_time, 1)
        dat5 = add_reconstructed_points_to_xyz(mountains_stable_file,
                                               rotation_model,
                                               reconstruction_time, 1)
        mountains_tr_point_array = np.vstack((dat3, dat4, dat5))

        # calculate distances of the mountain points to the edge of the mountain region at t1 and t2,
        # using the pg polygons that they should exactly correspond to
        dist_t1 = get_distance_to_mountain_edge(mountains_t1_point_array,
                                                reconstruction_basedir,
                                                rotation_model, time_stage_min,
                                                area_threshold)
        dist_t2 = get_distance_to_mountain_edge(mountains_t2_point_array,
                                                reconstruction_basedir,
                                                rotation_model, time_stage_max,
                                                area_threshold)

        #is_in_orogeny_index = find_mountain_type(mountains_tr_point_array,
        #                                         orogeny_feature_filename,
        #                                         reconstruction_time)

        # cap the distances at some arbitrary value defined earlier
        dist_t1_cap = np.array(dist_t1)
        dist_t1_cap[
            np.array(dist_t1) >
            mountain_buffer_distance_degrees] = mountain_buffer_distance_degrees

        dist_t1_cap = dist_t1_cap * mountains_t1_point_array[:, 2]

        dist_t2_cap = np.array(dist_t2)
        dist_t2_cap[
            np.array(dist_t2) >
            mountain_buffer_distance_degrees] = mountain_buffer_distance_degrees

        dist_t2_cap = dist_t2_cap * mountains_t2_point_array[:, 2]

        # get the normalised time within this time stage
        # for example we are at 0.25 between the t1 and t2
        t_diff = (time_stage_max - time_stage_min)
        t_norm = (reconstruction_time - time_stage_min) / t_diff

        # use 1d interpolation to get the 'normalized' height of the mountains at the preceding
        # and subsequent times to the specific reconstruction time
        # [note this is not spatial interpolation - rather it is interpolation at each individual point
        # between the heights at earlier and later times]
        tmp = np.vstack((dist_t1_cap, dist_t2_cap))
        f = interpolate.interp1d([0, 1], tmp.T)
        normalized_mountain_elevation = f(t_norm)

    # interpolate the elevations at tr onto the regular long lat points that we will ultimately use
    # for the grid output
    # note the k value here controls number of neighbouring points used in inverse distance average
    d, l = sampleOnSphere(mountains_tr_point_array[:, 1],
                          mountains_tr_point_array[:, 0],
                          normalized_mountain_elevation,
                          np.array(lon),
                          np.array(lat),
                          k=4)
    w = 1. / d**2
    normalized_mountain_elevation_interp_points = np.sum(
        w * normalized_mountain_elevation.ravel()[l], axis=1) / np.sum(w,
                                                                       axis=1)

    # this index isolates only those points that are within a certain distance of the mountain range
    # (since the interpolation will give values everywhere in the 'land', so we want to re-isolate only
    # those points that fall within the 'mountain' regions)
    # TODO this should be set to the sampling??
    mountain_proximity_index = np.degrees(np.min(
        d, axis=1)) < sampling * 2  #mountain_buffer_distance_degrees

    #####################################
    # Put the grid together
    #####################################

    # write the land/marine points to a file
    land_marine_xyz_file = tempfile.NamedTemporaryFile(delete=False)
    mountain_xyz_file = tempfile.NamedTemporaryFile(delete=False)
    land_marine_nc_file = tempfile.NamedTemporaryFile(delete=False)
    mountain_nc_file = tempfile.NamedTemporaryFile(delete=False)

    # Cannot open twice on Windows - close before opening again.
    land_marine_xyz_file.close()
    mountain_xyz_file.close()
    land_marine_nc_file.close()
    mountain_nc_file.close()

    write_xyz_file(
        land_marine_xyz_file.name,
        zip(lon + lon_deep, lat + lat_deep,
            np.hstack((land_marine_interp_points, zval_deep))))

    # convert the normalized mountain elevations to metres, then write to file
    mountain_elevation_factor = max_mountain_elevation / mountain_buffer_distance_degrees
    mountain_elevation_array = normalized_mountain_elevation_interp_points[
        mountain_proximity_index] * mountain_elevation_factor
    write_xyz_file(
        mountain_xyz_file.name,
        zip(
            np.array(lon)[mountain_proximity_index],
            np.array(lat)[mountain_proximity_index], mountain_elevation_array))

    # all the points are already on the same regular lat/long grid (but with gaps) - just
    # need to piece them all together and combine.
    # Note we assume the the mountain elevation is the height IN ADDITION to the lowland elevation
    # so that we can simply add them
    call_system_command([
        'gmt', 'xyz2grd', land_marine_xyz_file.name, '-Rd',
        '-I%0.8f' % sampling,
        '-G%s' % land_marine_nc_file.name
    ])
    call_system_command([
        'gmt', 'xyz2grd', mountain_xyz_file.name, '-Rd',
        '-I%0.8f' % sampling, '-di0',
        '-G%s' % mountain_nc_file.name
    ])
    call_system_command([
        'gmt', 'grdmath', mountain_nc_file.name, land_marine_nc_file.name,
        'ADD', '=',
        '%s/paleotopo_%0.2fd_%0.2fMa.nc' %
        (output_dir, sampling, reconstruction_time)
    ])

    # Remove temp file (because we set 'delete=False').
    os.unlink(land_marine_xyz_file.name)
    os.unlink(mountain_xyz_file.name)
    os.unlink(land_marine_nc_file.name)
    os.unlink(mountain_nc_file.name)

    # load result back into python
    topoX, topoY, topoZ = load_netcdf(
        '%s/paleotopo_%0.2fd_%0.2fMa.nc' %
        (output_dir, sampling, reconstruction_time))

    if merge_with_bathymetry:

        # TODO create seperate function for this step

        # PALEOBATHYMETRY based on age grids
        # load age grid for this time and calculate paleobathymetry
        agegrid_file = agegrid_file_template % reconstruction_time

        ageX, ageY, ageZ = load_netcdf(agegrid_file)

        paleodepth = pg.age2depth(ageZ, model='GDH1')

        # get index for grid nodes where age grid is nan, replace values with topography/shallow bathymetry
        land_or_ocean_precedence = 'land'
        if land_or_ocean_precedence is 'ocean':
            not_bathy_index = np.isnan(paleodepth)
            paleodepth[not_bathy_index] = topoZ[not_bathy_index]
        else:
            not_bathy_index = np.greater(topoZ, depth_for_unknown_ocean)
            paleodepth[not_bathy_index] = topoZ[not_bathy_index]
            leftover_nans = np.isnan(paleodepth)
            paleodepth[leftover_nans] = depth_for_unknown_ocean

        plt.figure()
        plt.imshow(not_bathy_index)
        plt.show()

        paleotopobathy_nc_file = tempfile.NamedTemporaryFile(delete=False)
        paleotopobathy_smooth_nc_file = tempfile.NamedTemporaryFile(
            delete=False)

        # Cannot open twice on Windows - close before opening again.
        paleotopobathy_nc_file.close()
        paleotopobathy_smooth_nc_file.close()

        # save the merged grid (forcing compatibility with GPlates-readable netCDF in case it helps)
        ds = xr.DataArray(paleodepth,
                          coords=[('lat', topoY), ('lon', topoX)],
                          name='elevation')
        ds.to_netcdf(paleotopobathy_nc_file.name, format='NETCDF3_CLASSIC')

        # smooth the grid using GMT [wavelength is optional
        #pg.smooth_topography_grid('paleotopobathy.nc','paleotopobathy_smooth_%0.2fMa.nc' % reconstruction_time,400.)
        # TODO skip this step if grid_smoothing_wavelength_kms set to zero
        call_system_command([
            'gmt', 'grdfilter', paleotopobathy_nc_file.name,
            '-G%s' % paleotopobathy_smooth_nc_file.name,
            '-Fg%0.2f' % grid_smoothing_wavelength_kms, '-fg', '-D4', '-Vl'
        ])

        if netcdf3_output:
            # finally, once again force GPlates-readable netCDF (ie netCDF v3) and put the
            # grid in the output folder with a filename containing the age
            call_system_command([
                'gmt', 'grdconvert', paleotopobathy_smooth_nc_file.name,
                '-G%s=cf' %
                '%s/paleotopobathy_buffer%0.2dd_filter_%0.2fkm_%0.2fMa.nc' %
                (output_dir, mountain_buffer_distance_degrees,
                 grid_smoothing_wavelength_kms, sampling, reconstruction_time)
            ])
        else:
            call_system_command([
                'cp', paleotopobathy_smooth_nc_file.name,
                '%s/paleotopobathy_buffer%0.2dd_filter%0.2fkm_%0.2fd_%0.2fMa.nc'
                %
                (output_dir, mountain_buffer_distance_degrees,
                 grid_smoothing_wavelength_kms, sampling, reconstruction_time)
            ])

        # load and plot the result
        topo_smoothX, topo_smoothY, topo_smoothZ = load_netcdf(
            paleotopobathy_smooth_nc_file.name)
        #
        plt.figure(figsize=(25, 11))
        plt.imshow(topo_smoothZ,
                   origin='lower',
                   extent=[-180, 180, -90, 90],
                   cmap=plt.cm.terrain,
                   vmin=-5000,
                   vmax=5000)
        plt.title('%0.2fMa' % reconstruction_time)
        plt.colorbar()
        plt.savefig(
            '%s/paleotopobathy_buffer%0.2dd_filter%0.2fkm_%0.2fd_%0.2fMa.png' %
            (output_dir, mountain_buffer_distance_degrees,
             grid_smoothing_wavelength_kms, sampling, reconstruction_time))
        plt.close()

        # Remove temp file (because we set 'delete=False').
        os.unlink(paleotopobathy_nc_file.name)
        os.unlink(paleotopobathy_smooth_nc_file.name)