Ejemplo n.º 1
0
    def get_multi_line_string(self, to_wkt=None, tolerance=0):
        '''\
        Function to return a shapely MultiLineString object representing the line dataset
        '''
        _line_indices, line_start_indices = np.sort(
            np.unique(self.line_index, return_index=True))

        line_end_indices = np.array(line_start_indices)
        line_end_indices[0:-1] = line_start_indices[1:]
        line_end_indices[-1] = self.point_count
        line_end_indices = line_end_indices - 1

        line_list = []
        for line_index in range(len(line_start_indices)):
            line_slice = slice(line_start_indices[line_index],
                               line_end_indices[line_index] + 1)
            line_vertices = self.xycoords[line_slice]
            line_vertices = line_vertices[~np.any(
                np.isnan(line_vertices), axis=1)]  # Discard null coordinates
            if len(
                    line_vertices
            ) >= 2:  # LineStrings must have at least 2 coordinate tuples
                line_list.append(
                    LineString(
                        transform_coords(line_vertices, self.wkt,
                                         to_wkt)).simplify(tolerance))

        return MultiLineString(line_list)
Ejemplo n.º 2
0
    def get_convex_hull(self, to_wkt=None):
        '''
        Function to return vertex coordinates of a convex hull polygon around all points
        '''
        convex_hull = points2convex_hull(self.xycoords)

        return transform_coords(convex_hull, self.wkt, to_wkt)
Ejemplo n.º 3
0
    def get_reprojected_bounds(self, bounds, from_wkt, to_wkt):
        '''
        Function to take a bounding box specified in one CRS and return its smallest containing bounding box in a new CRS
        @parameter bounds: bounding box specified as tuple(xmin, ymin, xmax, ymax) in CRS from_wkt
        @parameter from_wkt: WKT for CRS from which to transform bounds
        @parameter to_wkt: WKT for CRS to which to transform bounds
        
        @return reprojected_bounding_box: bounding box specified as tuple(xmin, ymin, xmax, ymax) in CRS to_wkt
        '''
        if (to_wkt is None) or (from_wkt is None) or (to_wkt == from_wkt):
            return bounds

        # Need to look at all four bounding box corners, not just LL & UR
        original_bounding_box = ((bounds[0], bounds[1]), (bounds[2],
                                                          bounds[1]),
                                 (bounds[2], bounds[3]), (bounds[0],
                                                          bounds[3]))
        reprojected_bounding_box = np.array(
            transform_coords(original_bounding_box, from_wkt, to_wkt))

        return [
            min(reprojected_bounding_box[:, 0]),
            min(reprojected_bounding_box[:, 1]),
            max(reprojected_bounding_box[:, 0]),
            max(reprojected_bounding_box[:, 1])
        ]
Ejemplo n.º 4
0
    def test_transform_coords(self):
        print('Testing transform_coords function with single coordinate{}'.
              format(TestCRSUtils.EPSG4326_COORDS))
        utm_coords = transform_coords(TestCRSUtils.EPSG4326_COORDS,
                                      TestCRSUtils.EPSG4326_WKT,
                                      TestCRSUtils.UTM_WKT)
        assert (utm_coords == np.array(TestCRSUtils.UTM_COORDS)
                ).all(), 'Incorrect UTM coordinates: {} instead of {}'.format(
                    utm_coords, TestCRSUtils.UTM_COORDS)

        print('Testing transform_coords function with multi coordinate {}'.
              format(TestCRSUtils.EPSG4326_COORD_ARRAY))
        utm_coord_array = transform_coords(TestCRSUtils.EPSG4326_COORD_ARRAY,
                                           TestCRSUtils.EPSG4326_WKT,
                                           TestCRSUtils.UTM_WKT)
        assert (utm_coord_array == np.array(TestCRSUtils.UTM_COORD_ARRAY)
                ).all(), 'Incorrect UTM coordinates: {} instead of {}'.format(
                    utm_coord_array, TestCRSUtils.UTM_COORD_ARRAY)
Ejemplo n.º 5
0
    def get_convex_hull(self, to_wkt=None):
        '''\
        Function to return n x 2 array of coordinates for convex hull based on line start/end points
        Implements abstract base function in NetCDFUtils 
        @param to_wkt: CRS WKT for shape
        '''
        points = transform_coords(self.get_line_sample_points(), self.wkt,
                                  to_wkt)

        try:
            convex_hull = points2convex_hull(points)
        except:
            #logger.info('Unable to compute convex hull. Using rectangular bounding box instead.')
            convex_hull = self.native_bbox

        return convex_hull
Ejemplo n.º 6
0
    def get_spatial_mask(self, bounds, bounds_wkt=None):
        '''
        Return boolean mask of dimension 'point' for all coordinates within specified bounds and CRS
        '''
        coordinates = self.xycoords

        if bounds_wkt is not None:
            coordinates = np.array(
                transform_coords(self.xycoords, self.wkt, bounds_wkt))

        bounds_half_size = abs(
            np.array([bounds[2] - bounds[0], bounds[3] - bounds[1]])) / 2.0
        bounds_centroid = np.array([bounds[0], bounds[1]]) + bounds_half_size

        # Return true for each point which is <= bounds_half_size distance from bounds_centroid
        return np.all(ne.evaluate(
            "abs(coordinates - bounds_centroid) <= bounds_half_size"),
                      axis=1)
Ejemplo n.º 7
0
def get_gdal_grid_values(gdal_dataset, sample_points, from_crs, band_no=1):
    '''
    Function to return values at a series of points from a GDAL dataset
    '''
    geotransform = gdal_dataset.GetGeoTransform()
    to_crs = gdal_dataset.GetProjection()
    gdal_band = gdal_dataset.GetRasterBand(band_no)
    
    native_sample_points = transform_coords(sample_points, from_crs, to_crs)
    
    #TODO: Make this faster
    values = []
    for point in native_sample_points:
        indices = (int((point[0] - geotransform[0]) / geotransform[1] + 0.5),
                   int((point[1] - geotransform[3]) / geotransform[5] + 0.5))
        value = gdal_band.ReadAsArray(xoff=indices[0], yoff=indices[1], win_xsize=1, win_ysize=1)[0,0]
        #print point, indices, value
        values.append(value)
        
    return np.array(values)
Ejemplo n.º 8
0
def grid_points_gdal(cond_point_utils_inst,
                     grid_resolution,
                     variables=None,
                     native_grid_bounds=None,
                     reprojected_grid_bounds=None,
                     grid_wkt=None,
                     point_step=1,
                     grid_kwargs=None,
                     depth_inds=None):
    '''
    Function that grids points in the cond_point_utils instance within a bounding box using gdal_grid.
    @parameter cond_point_utils_inst: instance of cond_point_utils from geophys_utils
    @parameter grid_resolution: cell size of regular grid in grid CRS units
    @parameter variables: Single variable name string or list of multiple variable name strings. Defaults to all point variables
    @parameter native_grid_bounds: Spatial bounding box of area to grid in native coordinates
    @parameter reprojected_grid_bounds: Spatial bounding box of area to grid in grid coordinates
    @parameter grid_wkt: WKT for grid coordinate reference system. Defaults to native CRS
    @parameter point_step: Sampling spacing for points. 1 (default) means every point, 2 means every second point, etc.
    @parameter depth inds: list of depth indices to grid.

    @return grid: dictionary with gridded data, geotransform and wkt
    '''
    assert not (
        native_grid_bounds and reprojected_grid_bounds
    ), 'Either native_grid_bounds or reprojected_grid_bounds can be provided, but not both'
    # Grid all data variables if not specified
    variables = variables or cond_point_utils_inst.point_variables

    # Allow single variable to be given as a string
    single_var = (type(variables) == str)
    if single_var:
        variables = [variables]

    if native_grid_bounds:
        reprojected_grid_bounds = cond_point_utils_inst.get_reprojected_bounds(
            native_grid_bounds, cond_point_utils_inst.wkt, grid_wkt)
    elif reprojected_grid_bounds:
        native_grid_bounds = cond_point_utils_inst.get_reprojected_bounds(
            reprojected_grid_bounds, grid_wkt, cond_point_utils_inst.wkt)
    else:  # No reprojection required
        native_grid_bounds = cond_point_utils_inst.bounds
        reprojected_grid_bounds = cond_point_utils_inst.bounds

    # Determine spatial grid bounds rounded out to nearest GRID_RESOLUTION multiple
    pixel_centre_bounds = (
        round(
            math.floor(reprojected_grid_bounds[0] / grid_resolution) *
            grid_resolution, 6),
        round(
            math.floor(reprojected_grid_bounds[1] / grid_resolution) *
            grid_resolution, 6),
        round(
            math.floor(reprojected_grid_bounds[2] / grid_resolution - 1.0) *
            grid_resolution + grid_resolution, 6),
        round(
            math.floor(reprojected_grid_bounds[3] / grid_resolution - 1.0) *
            grid_resolution + grid_resolution, 6))

    # Extend area for points an arbitrary two cells out beyond grid extents for nice interpolation at edges
    expanded_grid_bounds = [
        pixel_centre_bounds[0] - 2 * grid_resolution,
        pixel_centre_bounds[1] - 2 * grid_resolution,
        pixel_centre_bounds[2] + 2 * grid_resolution,
        pixel_centre_bounds[3] + 2 * grid_resolution
    ]

    expanded_grid_size = [
        expanded_grid_bounds[dim_index + 2] - expanded_grid_bounds[dim_index]
        for dim_index in range(2)
    ]

    # Get width and height of grid
    width = int(expanded_grid_size[0] / grid_resolution)
    height = int(expanded_grid_size[1] / grid_resolution)

    spatial_subset_mask = cond_point_utils_inst.get_spatial_mask(
        cond_point_utils_inst.get_reprojected_bounds(
            expanded_grid_bounds, grid_wkt, cond_point_utils_inst.wkt))

    # Skip points to reduce memory requirements
    # TODO: Implement function which grids spatial subsets.
    point_subset_mask = np.zeros(shape=(
        cond_point_utils_inst.netcdf_dataset.dimensions['point'].size, ),
                                 dtype=bool)
    point_subset_mask[0:-1:point_step] = True
    point_subset_mask = np.logical_and(spatial_subset_mask, point_subset_mask)

    coordinates = cond_point_utils_inst.xycoords[point_subset_mask]
    # Reproject coordinates if required
    if grid_wkt is not None:
        # N.B: Be careful about XY vs YX coordinate order
        coordinates = np.array(
            transform_coords(coordinates[:], cond_point_utils_inst.wkt,
                             grid_wkt))

    grid = {}
    for variable in [
            cond_point_utils_inst.netcdf_dataset.variables[var_name]
            for var_name in variables
    ]:

        # If preferences are not given we grid using defaults
        if grid_kwargs is None:
            grid_kwargs = {}
            grid_kwargs[variable.name] = {}

        if not 'log_grid' in grid_kwargs:
            grid_kwargs[variable.name]['log_grid'] = False

        # Check for the algorithm in the grid kwargs
        if not 'gdal_algorithm' in grid_kwargs:
            s = 'invdist:power=2:radius1=250:'
            s += 'radius2=250:max_points=15:'
            s += 'min_points=2:nodata=-32768.0'
            # Defaut
            grid_kwargs[variable.name]['gdal_algorithm'] = s

        if not 'format' in grid_kwargs:
            # Defaut
            grid_kwargs[variable.name]['format'] = 'GTiff'

        if not 'outputType' in grid_kwargs:
            # Defaut
            grid_kwargs[variable.name]['outputType'] = gdal.GDT_Float32

        grid_kwargs[variable.name]['width'] = width
        grid_kwargs[variable.name]['height'] = height

        grid_kwargs[variable.name]['outputSRS'] = grid_wkt
        grid_kwargs[variable.name]['outputBounds'] = expanded_grid_bounds

        # For 2d arrays
        if len(variable.shape) == 2:

            if depth_inds is not None:
                nlayers = len(depth_inds)
            else:
                nlayers = variable.shape[1]
                depth_inds = np.arange(0, nlayers)

            a = np.nan * np.ones(shape=(nlayers, int(height), int(width)),
                                 dtype=np.float32)

            # Iterate through the layers
            for i, ind in enumerate(depth_inds):
                var_array = variable[point_subset_mask, ind].reshape([-1, 1])

                a[i], geotransform = grid_var(var_array, coordinates,
                                              grid_kwargs[variable.name])

        elif len(variable.shape) == 1:

            # Create a temporary array
            var_array = variable[point_subset_mask].reshape([-1, 1])

            a, geotransform = grid_var(var_array, coordinates,
                                       grid_kwargs[variable.name])

        grid[variable.name] = a

    grid['geotransform'] = geotransform
    grid['wkt'] = grid_wkt

    return grid
Ejemplo n.º 9
0
    def nearest_neighbours(self,
                           coordinates,
                           wkt=None,
                           points_required=1,
                           max_distance=None,
                           secondary_mask=None):
        '''
        Function to determine nearest neighbours using cKDTree
        N.B: All distances are expressed in the native dataset CRS
        
        @param coordinates: two-element XY coordinate tuple, list or array
        @param wkt: Well-known text of coordinate CRS - defaults to native dataset CRS
        @param points_required: Number of points to retrieve. Default=1
        @param max_distance: Maximum distance to search from target coordinate - 
            STRONGLY ADVISED TO SPECIFY SENSIBLE VALUE OF max_distance TO LIMIT SEARCH AREA
        @param secondary_mask: Boolean array of same shape as point array used to filter points. None = no filter.
        
        @return distances: distances from the target coordinate for each of the points_required nearest points
        @return indices: point indices for each of the points_required nearest points
        '''
        if wkt:
            reprojected_coords = transform_coords(coordinates, wkt, self.wkt)
        else:
            reprojected_coords = coordinates

        if secondary_mask is None:
            secondary_mask = np.ones(shape=(self.point_count, ), dtype=bool)
        else:
            assert secondary_mask.shape == (self.point_count, )

        if max_distance:  # max_distance has been specified
            logger.debug('Computing spatial subset mask...')
            spatial_mask = self.get_spatial_mask([
                reprojected_coords[0] - max_distance,
                reprojected_coords[1] - max_distance,
                reprojected_coords[0] + max_distance,
                reprojected_coords[1] + max_distance
            ])

            point_indices = np.where(
                np.logical_and(spatial_mask, secondary_mask))[0]

            if not len(point_indices):
                logger.debug('No points within distance {} of {}'.format(
                    max_distance, reprojected_coords))
                return [], []

            # Set up KDTree for nearest neighbour queries
            logger.debug(
                'Indexing spatial subset with {} points into KDTree...'.format(
                    np.count_nonzero(spatial_mask)))
            kdtree = cKDTree(data=self.xycoords[point_indices])
            logger.debug('Finished indexing spatial subset into KDTree.')
        else:  # Consider ALL points
            max_distance = np.inf
            kdtree = self.kdtree

        distances, indices = kdtree.query(x=np.array(reprojected_coords),
                                          k=points_required,
                                          distance_upper_bound=max_distance)

        if max_distance == np.inf:
            return distances, indices
        else:  # Return indices of complete coordinate array, not the spatial subset
            return distances, np.where(spatial_mask)[0][indices]
Ejemplo n.º 10
0
    def grid_points(self,
                    grid_resolution,
                    variables=None,
                    native_grid_bounds=None,
                    reprojected_grid_bounds=None,
                    resampling_method='linear',
                    grid_wkt=None,
                    point_step=1):
        '''
        Function to grid points in a specified bounding rectangle to a regular grid of the specified resolution and crs
        @parameter grid_resolution: cell size of regular grid in grid CRS units
        @parameter variables: Single variable name string or list of multiple variable name strings. Defaults to all point variables
        @parameter native_grid_bounds: Spatial bounding box of area to grid in native coordinates 
        @parameter reprojected_grid_bounds: Spatial bounding box of area to grid in grid coordinates
        @parameter resampling_method: Resampling method for gridding. 'linear' (default), 'nearest' or 'cubic'. 
        See https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.griddata.html 
        @parameter grid_wkt: WKT for grid coordinate reference system. Defaults to native CRS
        @parameter point_step: Sampling spacing for points. 1 (default) means every point, 2 means every second point, etc.
        
        @return grids: dict of grid arrays keyed by variable name if parameter 'variables' value was a list, or
        a single grid array if 'variable' parameter value was a string
        @return wkt: WKT for grid coordinate reference system.
        @return geotransform: GDAL GeoTransform for grid
        '''
        assert not (
            native_grid_bounds and reprojected_grid_bounds
        ), 'Either native_grid_bounds or reprojected_grid_bounds can be provided, but not both'
        # Grid all data variables if not specified
        variables = variables or self.point_variables

        # Allow single variable to be given as a string
        single_var = (type(variables) == str)
        if single_var:
            variables = [variables]

        if native_grid_bounds:
            reprojected_grid_bounds = self.get_reprojected_bounds(
                native_grid_bounds, self.wkt, grid_wkt)
        elif reprojected_grid_bounds:
            native_grid_bounds = self.get_reprojected_bounds(
                reprojected_grid_bounds, grid_wkt, self.wkt)
        else:  # No reprojection required
            native_grid_bounds = self.bounds
            reprojected_grid_bounds = self.bounds

        # Determine spatial grid bounds rounded out to nearest GRID_RESOLUTION multiple
        pixel_centre_bounds = (
            round(
                math.floor(reprojected_grid_bounds[0] / grid_resolution) *
                grid_resolution, 6),
            round(
                math.floor(reprojected_grid_bounds[1] / grid_resolution) *
                grid_resolution, 6),
            round(
                math.floor(reprojected_grid_bounds[2] / grid_resolution - 1.0)
                * grid_resolution + grid_resolution, 6),
            round(
                math.floor(reprojected_grid_bounds[3] / grid_resolution - 1.0)
                * grid_resolution + grid_resolution, 6))

        grid_size = [
            pixel_centre_bounds[dim_index + 2] - pixel_centre_bounds[dim_index]
            for dim_index in range(2)
        ]

        # Extend area for points an arbitrary 4% out beyond grid extents for nice interpolation at edges
        expanded_grid_bounds = [
            pixel_centre_bounds[0] - grid_size[0] / 50.0,
            pixel_centre_bounds[1] - grid_size[0] / 50.0,
            pixel_centre_bounds[2] + grid_size[1] / 50.0,
            pixel_centre_bounds[3] + grid_size[1] / 50.0
        ]

        spatial_subset_mask = self.get_spatial_mask(
            self.get_reprojected_bounds(expanded_grid_bounds, grid_wkt,
                                        self.wkt))

        # Create grids of Y and X values. Note YX ordering and inverted Y
        # Note GRID_RESOLUTION/2.0 fudge to avoid truncation due to rounding error
        grid_y, grid_x = np.mgrid[
            pixel_centre_bounds[3]:pixel_centre_bounds[1] -
            grid_resolution / 2.0:-grid_resolution,
            pixel_centre_bounds[0]:pixel_centre_bounds[2] +
            grid_resolution / 2.0:grid_resolution]

        # Skip points to reduce memory requirements
        #TODO: Implement function which grids spatial subsets.
        point_subset_mask = np.zeros(
            shape=(self.netcdf_dataset.dimensions['point'].size, ), dtype=bool)
        point_subset_mask[0:-1:point_step] = True
        point_subset_mask = np.logical_and(spatial_subset_mask,
                                           point_subset_mask)

        coordinates = self.xycoords[point_subset_mask]
        # Reproject coordinates if required
        if grid_wkt is not None:
            # N.B: Be careful about XY vs YX coordinate order
            coordinates = np.array(
                transform_coords(coordinates[:], self.wkt, grid_wkt))

        # Interpolate required values to the grid - Note YX ordering for image
        grids = {}
        for variable in [
                self.netcdf_dataset.variables[var_name]
                for var_name in variables
        ]:
            grids[variable.name] = griddata(
                coordinates[:, ::-1],
                variable[:]
                [point_subset_mask],  #TODO: Check why this is faster than direct indexing
                (grid_y, grid_x),
                method=resampling_method)

        if single_var:
            grids = list(grids.values())[0]

        #  crs:GeoTransform = "109.1002342895272 0.00833333 0 -9.354948067227777 0 -0.00833333 "
        geotransform = [
            pixel_centre_bounds[0] - grid_resolution / 2.0, grid_resolution, 0,
            pixel_centre_bounds[3] + grid_resolution / 2.0, 0, -grid_resolution
        ]

        return grids, (grid_wkt or self.wkt), geotransform
Ejemplo n.º 11
0
def main():
    '''
    Main function
    '''
    def get_xml_text(xml_template_path, metadata_object):
        '''Helper function to perform substitutions on XML template text
        '''
        template_dir = os.path.join(
            os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
            'templates')
        jinja_environment = Environment(
            loader=FileSystemLoader(template_dir or './'),
            autoescape=select_autoescape(['html', 'xml']))

        xml_template = jinja_environment.get_template(xml_template_path,
                                                      parent=None)

        value_dict = dict(
            metadata_object.metadata_dict['Template'])  # Copy template values

        # Convert multiple sets of comma-separated lists to lists of strings to a list of dicts
        #TODO: Make this slicker
        value_dict['keywords'] = []
        for keyword_list_key in [
                key for key in value_dict.keys()
                if re.match('^KEYWORD_\w+_LIST$', key)
        ]:
            keywords = [
                keyword.strip()
                for keyword in value_dict[keyword_list_key].split(',')
            ]
            keyword_code = value_dict[re.sub('_LIST$', '_CODE',
                                             keyword_list_key)]

            value_dict['keywords'] += [{
                'value': keyword,
                'code': keyword_code
            } for keyword in keywords]

        # Create dict containing distribution info for DOI if required
        value_dict['distributions'] = []
        dataset_doi = metadata_object.get_metadata(['Calculated', 'DOI'])
        if dataset_doi:
            try:
                distribution_dict = {
                    'formatSpecification':
                    'html',
                    'distributor_name':
                    metadata_object.get_metadata(
                        ['Template', 'ORGANISATION_NAME']),
                    'distributor_telephone':
                    metadata_object.get_metadata(
                        ['Template', 'ORGANISATION_PHONE']),
                    'distributor_address':
                    metadata_object.get_metadata(
                        ['Template', 'ORGANISATION_ADDRESS']),
                    'distributor_city':
                    metadata_object.get_metadata(
                        ['Template', 'ORGANISATION_CITY']),
                    'distributor_state':
                    metadata_object.get_metadata(
                        ['Template', 'ORGANISATION_STATE']),
                    'distributor_postcode':
                    metadata_object.get_metadata(
                        ['Template', 'ORGANISATION_POSTCODE']),
                    'distributor_country':
                    metadata_object.get_metadata(
                        ['Template', 'ORGANISATION_COUNTRY']),
                    'distributor_email':
                    metadata_object.get_metadata(
                        ['Template', 'ORGANISATION_EMAIL']),
                    'url':
                    dataset_doi,
                    'protocol':
                    'WWW:LINK-1.0-http--link',
                    'name':
                    'Digital Object Identifier for dataset %s' %
                    metadata_object.get_metadata(['Calculated', 'UUID']),
                    'description':
                    'Dataset DOI'
                }

                for key, value in distribution_dict.iteritems():
                    assert value, '%s has no value defined' % key

                value_dict['distributions'].append(distribution_dict)
            except Exception as e:
                print 'WARNING: Unable to create DOI distribution: %s' % e.message

        return xml_template.render(**value_dict)

    def str2datelist(multi_date_string):
        '''
        Helper function to convert comma-separated string containing dates to a list of dates
        '''
        date_list = []
        for datetime_string in multi_date_string.split(','):
            for format in ['%d-%b-%y', '%Y-%m-%dT%H:%M:%S']:
                try:
                    date_list.append(
                        datetime.strptime(datetime_string.strip(),
                                          format).date())
                except:
                    continue
        return date_list

    # Start of main function
    assert len(sys.argv) >= 4 and len(
        sys.argv
    ) <= 8, 'Usage: %s <json_text_template_path> <xml_template_path> <netcdf_path> [<xml_output_dir>]' % sys.argv[
        0]
    json_text_template_path = sys.argv[1]
    xml_template_path = sys.argv[2]
    netcdf_path = sys.argv[3]
    if len(sys.argv) >= 5:
        xml_dir = sys.argv[4]
    else:
        xml_dir = '.'

    # Optional arguments for DB connection - not required at NCI
    if len(sys.argv) == 8:
        db_user = sys.argv[5]
        db_password = sys.argv[6]
        db_alias = sys.argv[7]
    else:
        db_user = None
        db_password = None
        db_alias = None

    xml_path = os.path.abspath(
        os.path.join(
            xml_dir,
            os.path.splitext(os.path.basename(netcdf_path))[0] + '.xml'))
    print xml_dir, xml_path

    metadata_object = Metadata()

    netcdf_metadata = NetCDFMetadata(netcdf_path)
    metadata_object.merge_root_metadata_from_object(netcdf_metadata)

    nc_dataset = netCDF4.Dataset(
        netcdf_path, 'r+')  # Allow for updating of netCDF attributes like uuid

    # JetCat and Survey metadata can either take a list of survey IDs as source(s) or a filename from which to parse them
    try:
        survey_ids = nc_dataset.survey_id
        print 'Survey ID "%s" found in netCDF attributes' % survey_ids
        source = [
            int(value_string.strip()) for value_string in survey_ids.split(',')
            if value_string.strip()
        ]
    except:
        source = netcdf_path


#    jetcat_metadata = JetCatMetadata(source, jetcat_path=jetcat_path)
#    metadata_object.merge_root_metadata_from_object(jetcat_metadata)

    try:
        survey_metadata = SurveyMetadata(source)
        metadata_object.merge_root_metadata_from_object(survey_metadata)
    except Exception as e:
        print 'Unable to read from Survey API:\n%s\nAttempting direct Oracle DB read' % e.message
        try:
            survey_metadata = ArgusMetadata(
                db_user, db_password, db_alias, source
            )  # This will fail if we haven't been able to import ArgusMetadata
            metadata_object.merge_root_metadata(
                'Survey', survey_metadata.metadata_dict,
                overwrite=True)  # Fake Survey metadata from DB query
        except Exception as e:
            print 'Unable to perform direct Oracle DB read: %s' % e.message

    nc_grid_utils = NetCDFGridUtils(nc_dataset)

    # Add some calculated values to the metadata
    calculated_values = {}
    metadata_object.metadata_dict['Calculated'] = calculated_values

    calculated_values['FILENAME'] = os.path.basename(netcdf_path)

    #calculated_values['CELLSIZE'] = str((nc_grid_utils.pixel_size[0] + nc_grid_utils.pixel_size[1]) / 2.0)
    calculated_values['CELLSIZE_M'] = str(
        int(
            round((nc_grid_utils.nominal_pixel_metres[0] +
                   nc_grid_utils.nominal_pixel_metres[1]) / 20.0) * 10))
    calculated_values['CELLSIZE_DEG'] = str(
        round((nc_grid_utils.nominal_pixel_degrees[0] +
               nc_grid_utils.nominal_pixel_degrees[1]) / 2.0, 8))

    try:
        calculated_values['START_DATE'] = min(
            str2datelist(
                str(metadata_object.get_metadata(['Survey',
                                                  'STARTDATE'])))).isoformat()
    except ValueError:
        calculated_values['START_DATE'] = None

    try:
        calculated_values['END_DATE'] = max(
            str2datelist(
                str(metadata_object.get_metadata(['Survey',
                                                  'ENDDATE'])))).isoformat()
    except ValueError:
        calculated_values['END_DATE'] = None

    # Find survey year from end date isoformat string
    try:
        calculated_values['YEAR'] = re.match(
            '^(\d{4})-', calculated_values['END_DATE']).group(1)
    except:
        calculated_values['YEAR'] = 'UNKNOWN'

    #history = "Wed Oct 26 14:34:42 2016: GDAL CreateCopy( /local/el8/axi547/tmp/mWA0769_770_772_773.nc, ... )"
    #date_modified = "2016-08-29T10:51:42"
    try:
        try:
            conversion_datetime_string = re.match(
                '^(.+):.*',
                str(metadata_object.get_metadata(['NetCDF',
                                                  'history']))).group(1)
            conversion_datetime_string = datetime.strptime(
                conversion_datetime_string,
                '%a %b %d %H:%M:%S %Y').isoformat()
        except:
            conversion_datetime_string = metadata_object.get_metadata(
                ['NetCDF', 'date_modified']) or 'UNKNOWN'
    except:
        conversion_datetime_string = 'UNKNOWN'

    calculated_values['CONVERSION_DATETIME'] = conversion_datetime_string

    survey_id = str(metadata_object.get_metadata(['Survey', 'SURVEYID']))
    try:
        dataset_survey_id = str(nc_dataset.survey_id)
        assert (set([
            int(value_string.strip())
            for value_string in dataset_survey_id.split(',')
            if value_string.strip()
        ]) == set([
            int(value_string.strip()) for value_string in survey_id.split(',')
            if value_string.strip()
        ])), 'NetCDF survey ID %s is inconsistent with %s' % (
            dataset_survey_id, survey_id)
    except:
        nc_dataset.survey_id = str(survey_id)
        nc_dataset.sync()
        print 'Survey ID %s written to netCDF file' % survey_id

    dataset_uuid = metadata_object.get_metadata(['NetCDF', 'uuid'])
    if not dataset_uuid:  # Create a new UUID and write it to the netCDF file
        dataset_uuid = uuid.uuid4()
        nc_dataset.uuid = dataset_uuid
        nc_dataset.sync()
        print 'Fresh UUID %s generated and written to netCDF file' % dataset_uuid

    calculated_values['UUID'] = str(dataset_uuid)

    dataset_doi = metadata_object.get_metadata(['NetCDF', 'doi'])
    if not dataset_doi and False:  #TODO: Mint a new DOI and write it to the netCDF file
        dataset_doi = ''  #TODO: Replace this with call to DOI minter - might be problematic from a non-GA source address
        nc_dataset.doi = dataset_doi
        nc_dataset.sync()
        print 'Fresh DOI %s generated and written to netCDF file' % dataset_uuid

    if dataset_doi:
        calculated_values['DOI'] = str(dataset_doi)

    WGS84_bbox = transform_coords(nc_grid_utils.native_bbox, nc_grid_utils.crs,
                                  'EPSG:4326')
    WGS84_extents = [
        min([coordinate[0] for coordinate in WGS84_bbox]),
        min([coordinate[1] for coordinate in WGS84_bbox]),
        max([coordinate[0] for coordinate in WGS84_bbox]),
        max([coordinate[1] for coordinate in WGS84_bbox])
    ]

    calculated_values['ELON'] = str(WGS84_extents[0])
    calculated_values['SLAT'] = str(WGS84_extents[1])
    calculated_values['WLON'] = str(WGS84_extents[2])
    calculated_values['NLAT'] = str(WGS84_extents[3])

    #template_class = None
    template_metadata_object = TemplateMetadata(json_text_template_path,
                                                metadata_object)
    metadata_object.merge_root_metadata_from_object(template_metadata_object)

    #pprint(metadata_object.metadata_dict)

    xml_text = get_xml_text(xml_template_path, metadata_object)
    #print xml_text
    xml_file = open(xml_path, 'w')
    xml_file.write(xml_text)
    xml_file.close()
    print 'XML written to %s' % xml_path