def fillInLevel1(self, metaDict): """ I wrote this code back in 2018, when it worked fine. I am guessing that all I had to test on was a Level-1 file, which we now no longer wish to process. I have left this code here for completeness. It may be completely obsolete. """ self.productType = metaDict[ 'METADATA_GRANULE_DESCRIPTION_ProductShortName'] startTimeStr = metaDict['time_coverage_start'] self.startTime = datetime.datetime.strptime(startTimeStr, "%Y-%m-%dT%H:%M:%SZ") stopTimeStr = metaDict['time_coverage_end'] self.stopTime = datetime.datetime.strptime(stopTimeStr, "%Y-%m-%dT%H:%M:%SZ") # And the generic time stamp, halfway between start and stop duration = self.stopTime - self.startTime self.datetime = self.startTime + datetime.timedelta(duration.days / 2) self.instrument = metaDict['sensor'] self.satId = metaDict['platform'] creationTimeStr = metaDict['date_created'] self.generationTime = datetime.datetime.strptime( creationTimeStr, "%Y-%m-%dT%H:%M:%SZ") self.processingSoftwareVersion = metaDict['processor_version'] # Leaving this as a string, in case they assume it later. It is a string in # sen2meta. self.processingLevel = metaDict[ 'METADATA_GRANULE_DESCRIPTION_ProcessLevel'] self.absoluteOrbitNumber = int(metaDict['orbit']) # Make an attempt at the footprint outline. Stole most of this from sen3meta. # Not yet sure whether most S5P products will be swathe products, or if there # will be some which are chopped up further. posListStr = metaDict[ 'METADATA_EOP_METADATA_om:featureOfInterest_eop:multiExtentOf_gml:surfaceMembers_gml:exterior_gml:posList'] posListStrVals = posListStr.split() numVals = len(posListStrVals) # Note that a gml:posList has pairs in order [lat long ....], with no sensible pair delimiter posListPairs = [ "{} {}".format(posListStrVals[i + 1], posListStrVals[i]) for i in range(0, numVals, 2) ] posListVals = [[float(x), float(y)] for (x, y) in [pair.split() for pair in posListPairs]] footprintGeom = geomutils.geomFromOutlineCoords(posListVals) prefEpsg = geomutils.findSensibleProjection(footprintGeom) if prefEpsg is not None: self.centroidXY = geomutils.findCentroid(footprintGeom, prefEpsg) else: self.centroidXY = None self.outlineWKT = footprintGeom.ExportToWkt() # Currently have no mechanism for a preview image self.previewImgBin = None
def __init__(self, xmlStr=None, xmlfilename=None, zipfilename=None): if xmlStr is None: if xmlfilename is not None: xmlStr = open(xmlfilename).read() elif zipfilename is not None: zf = zipfile.ZipFile(zipfilename, 'r') filenames = [zi.filename for zi in zf.infolist()] metadataXmlfile = [ fn for fn in filenames if fn.endswith('xfdumanifest.xml') ][0] mf = zf.open(metadataXmlfile) xmlStr = mf.read() del mf doc = minidom.parseString(xmlStr) xfduNode = doc.getElementsByTagName('xfdu:XFDU')[0] metadataSectionNode = xfduNode.getElementsByTagName( 'metadataSection')[0] metadataNodeList = metadataSectionNode.getElementsByTagName( 'metadataObject') # Acquisition times acquisitionPeriodNode = self.findMetadataNodeByIdName( metadataNodeList, 'acquisitionPeriod') startTimeNode = acquisitionPeriodNode.getElementsByTagName( 'sentinel-safe:startTime')[0] startTimeStr = startTimeNode.firstChild.data.strip() self.startTime = datetime.datetime.strptime(startTimeStr, "%Y-%m-%dT%H:%M:%S.%fZ") stopTimeNode = acquisitionPeriodNode.getElementsByTagName( 'sentinel-safe:stopTime')[0] stopTimeStr = stopTimeNode.firstChild.data.strip() self.stopTime = datetime.datetime.strptime(stopTimeStr, "%Y-%m-%dT%H:%M:%S.%fZ") # Platform details platformNode = self.findMetadataNodeByIdName(metadataNodeList, 'platform') familyNameNodeList = platformNode.getElementsByTagName( 'sentinel-safe:familyName') satFamilyNameNode = [ node for node in familyNameNodeList if node.getAttribute('abbreviation') == '' ][0] satFamilyNameStr = satFamilyNameNode.firstChild.data.strip() satNumberNode = platformNode.getElementsByTagName( 'sentinel-safe:number')[0] satNumberStr = satNumberNode.firstChild.data.strip() if satFamilyNameStr == "Sentinel-3": self.satId = "S3" + satNumberStr else: raise Sen3MetaError( "Satellite family = '{}', does not appear to be Sentinel-3". format(satFamilyNameStr)) instrumentNode = platformNode.getElementsByTagName( 'sentinel-safe:instrument')[0] instrFamilyNameNode = instrumentNode.getElementsByTagName( 'sentinel-safe:familyName')[0] self.instrument = instrFamilyNameNode.getAttribute('abbreviation') # Footprint. Confusingly, this is stored under the measurementFrameSet metadata node. frameSetNode = self.findMetadataNodeByIdName(metadataNodeList, 'measurementFrameSet') posListNode = frameSetNode.getElementsByTagName('gml:posList')[0] posListStr = posListNode.firstChild.data.strip() posListStrVals = posListStr.split() numVals = len(posListStrVals) # Note that a gml:posList has pairs in order [lat long ....], with no sensible pair delimiter posListPairs = [ "{} {}".format(posListStrVals[i + 1], posListStrVals[i]) for i in range(0, numVals, 2) ] posListVals = [[float(x), float(y)] for (x, y) in [pair.split() for pair in posListPairs]] footprintGeom = geomutils.geomFromOutlineCoords(posListVals) prefEpsg = geomutils.findSensibleProjection(footprintGeom) if prefEpsg is not None: self.centroidXY = geomutils.findCentroid(footprintGeom, prefEpsg) else: self.centroidXY = None self.outlineWKT = footprintGeom.ExportToWkt() # Frame, which is not stored in the measurementFrameSet node, but in # the generalProductInfo node. prodInfoNode = self.findMetadataNodeByIdName( metadataNodeList, 'generalProductInformation') frameNodeList = prodInfoNode.getElementsByTagName( 'sentinel3:alongtrackCoordinate') self.frameNumber = None if len(frameNodeList) > 0: frameNode = frameNodeList[0] self.frameNumber = int(frameNode.firstChild.data.strip()) # Processing level productTypeNode = prodInfoNode.getElementsByTagName( 'sentinel3:productType')[0] self.productType = productTypeNode.firstChild.data.strip() self.processingLevel = self.productType[3] self.productName = self.productType[5:] # Product creation/processing time. Note that they use a different time format (sigh.....) creationTimeNode = prodInfoNode.getElementsByTagName( 'sentinel3:creationTime')[0] generationTimeStr = creationTimeNode.firstChild.data.strip() self.generationTime = datetime.datetime.strptime( generationTimeStr, "%Y%m%dT%H%M%S") # I think this is as close as we get to a software version number. baselineNode = prodInfoNode.getElementsByTagName( 'sentinel3:baselineCollection')[0] self.baselineCollection = baselineNode.firstChild.data.strip() # Orbit number orbitRefNode = self.findMetadataNodeByIdName( metadataNodeList, 'measurementOrbitReference') relativeOrbitNode = orbitRefNode.getElementsByTagName( 'sentinel-safe:relativeOrbitNumber')[0] self.relativeOrbitNumber = int( relativeOrbitNode.firstChild.data.strip()) absoluteOrbitNode = orbitRefNode.getElementsByTagName( 'sentinel-safe:orbitNumber')[0] self.absoluteOrbitNumber = int( absoluteOrbitNode.firstChild.data.strip()) cycleNode = orbitRefNode.getElementsByTagName( 'sentinel-safe:cycleNumber')[0] self.cycleNumber = int(cycleNode.firstChild.data.strip()) # MD5 checksum for .nc files dataSectionNode = xfduNode.getElementsByTagName('dataObjectSection')[0] dataList = dataSectionNode.getElementsByTagName('dataObject') md5 = {} for dataObject in dataList: key = dataObject.getElementsByTagName( 'fileLocation')[0].getAttribute('href') value = dataObject.getElementsByTagName( 'checksum')[0].firstChild.data.strip() md5[key] = value self.md5 = md5 # Currently have no mechanism for a preview image self.previewImgBin = None
def __init__(self, zipfilename=None): """ Currently only operates on the zipfile itself. """ if zipfilename is None: raise Sen1MetaError("Must give zipfilename") zf = zipfile.ZipFile(zipfilename, 'r') filenames = [zi.filename for zi in zf.infolist()] safeDirName = [fn for fn in filenames if fn.endswith('.SAFE/')][0] bn = safeDirName.replace('.SAFE/', '') #use manifest.safe metafilename = 'manifest.safe' fullmetafilename = safeDirName + metafilename mf = zf.open(fullmetafilename) xmlStr = mf.read() del mf doc = minidom.parseString(xmlStr) xfduNode = doc.getElementsByTagName('xfdu:XFDU')[0] metadataSectionNode = xfduNode.getElementsByTagName('metadataSection')[0] metadataNodeList = metadataSectionNode.getElementsByTagName('metadataObject') # Product information generalProductInformation = self.findMetadataNodeByIdName(metadataNodeList, 'generalProductInformation') productInformation = self.getElementsContainTagName(generalProductInformation,'standAloneProductInformation')[0] productTypeNodes=self.getElementsContainTagName(productInformation,'productType') if len(productTypeNodes)>0: self.productType= productTypeNodes[0].firstChild.data.strip() else: #this may happen if product type is RAW self.productType= os.path.basename(zipfilename).split("_")[2] self.polarisation = sorted([node.firstChild.data.strip() for node in self.getElementsContainTagName(productInformation,'transmitterReceiverPolarisation')]) #productInformation = generalProductInformation.getElementsByTagName('s1sarl1:standAloneProductInformation')[0] #self.productType = productInformation.getElementsByTagName('s1sarl1:productType')[0].firstChild.data.strip() #self.polarisation = sorted([node.firstChild.data.strip() for node in productInformation.getElementsByTagName('s1sarl1:transmitterReceiverPolarisation')]) # Acquisition times acquisitionPeriodNode = self.findMetadataNodeByIdName(metadataNodeList, 'acquisitionPeriod') startTimeNode = self.getElementsContainTagName(acquisitionPeriodNode,'startTime')[0] startTimeStr = startTimeNode.firstChild.data.strip() if 'Z' in startTimeStr[-1]: self.startTime = datetime.datetime.strptime(startTimeStr, "%Y-%m-%dT%H:%M:%S.%fZ") else: self.startTime = datetime.datetime.strptime(startTimeStr, "%Y-%m-%dT%H:%M:%S.%f") stopTimeNode = self.getElementsContainTagName(acquisitionPeriodNode,'stopTime')[0] stopTimeStr = stopTimeNode.firstChild.data.strip() if 'Z' in startTimeStr[-1]: self.stopTime = datetime.datetime.strptime(stopTimeStr, "%Y-%m-%dT%H:%M:%S.%fZ") else: self.stopTime = datetime.datetime.strptime(stopTimeStr, "%Y-%m-%dT%H:%M:%S.%f") # ESA processing time processingNodeList = findElementByXPath(xfduNode, "metadataObject/metadataWrap/xmlData/safe:processing") if len(processingNodeList) == 0: # The RAW product has a slightly different tag name - sigh..... processingNodeList = findElementByXPath(xfduNode, "metadataObject/metadataWrap/xmlData/processing") self.generationTime = None self.processingSoftwareVersion = None if len(processingNodeList) > 0: processingNode = processingNodeList[0] processingStartTimeStr = processingNode.getAttribute('start') self.generationTime = datetime.datetime.strptime(processingStartTimeStr, "%Y-%m-%dT%H:%M:%S.%f") softwareNodeList = findElementByXPath(processingNode, "safe:software") if len(softwareNodeList) == 0: # This seems to be an old name for the software tag softwareNodeList = findElementByXPath(processingNode, "software") # The RAW product does not seem to have this under either name. Sigh..... if len(softwareNodeList) > 0: self.processingSoftwareVersion = softwareNodeList[0].getAttribute('version') # platform platform = self.findMetadataNodeByIdName(metadataNodeList, 'platform') platformNode = self.getElementsContainTagName(platform,'platform')[0] familyName = self.getElementsContainTagName(platformNode,'familyName')[0].firstChild.data.strip() # to be consistent with other metadata, this has to be "S1" not "Sentinel-1" self.satId = familyName[0]+ familyName[-1]+self.getElementsContainTagName(platformNode,'number')[0].firstChild.data.strip() instrumentMode = self.getElementsContainTagName(platform,'instrumentMode')[0] self.mode = self.getElementsContainTagName(instrumentMode,'mode')[0].firstChild.data.strip() self.swath = sorted([node.firstChild.data.strip() for node in self.getElementsContainTagName(instrumentMode,'swath')]) # orbit measurementOrbitReference = self.findMetadataNodeByIdName(metadataNodeList, 'measurementOrbitReference') orbitReference = self.getElementsContainTagName(measurementOrbitReference,'orbitReference')[0] self.absoluteOrbitNumber = self.getElementsContainTagName(orbitReference,'orbitNumber')[0].firstChild.data.strip() self.relativeOrbitNumber = self.getElementsContainTagName(orbitReference,'relativeOrbitNumber')[0].firstChild.data.strip() self.passDirection = measurementOrbitReference.getElementsByTagName('s1:orbitProperties')[0].getElementsByTagName('s1:pass')[0].firstChild.data.strip().title() # footprint measurementFrameSet = self.findMetadataNodeByIdName(metadataNodeList, 'measurementFrameSet') posSet = self.getElementsContainTagName(measurementFrameSet,'coordinates') # first footprint posListStr = posSet[0].firstChild.data.strip() # This list has pairs in order [lat,long lat,long....], different from S1 and S3 posListPairs= posListStr.split() posListVals = [[float(y), float(x)] for (x, y) in [pair.split(',') for pair in posListPairs]] footprintGeom = geomutils.geomFromOutlineCoords(posListVals) footprintGeom.CloseRings() # there are more than one polygons for WV products if len(posSet)>1: footprintGeom = geomutils.ogr.ForceToMultiPolygon(footprintGeom) for pos in posSet[1:]: posListPairs= pos.firstChild.data.strip().split() posListVals = [[float(y), float(x)] for (x, y) in [pair.split(',') for pair in posListPairs]] footprint = geomutils.geomFromOutlineCoords(posListVals) footprint.CloseRings() footprintGeom.AddGeometry(footprint) #footprintGeom=footprintGeom.ConvexHull() self.centroidXY = None if footprintGeom.GetGeometryName().upper() == 'POLYGON': prefEpsg = geomutils.findSensibleProjection(footprintGeom) if prefEpsg is not None: self.centroidXY = geomutils.findCentroid(footprintGeom, prefEpsg) self.outlineWKT = footprintGeom.ExportToWkt() # Grab preview data if available, for making a quick-look previewDir = os.path.join(safeDirName, "preview") previewImgFiles = [fn for fn in filenames if os.path.dirname(fn) == previewDir and fn.endswith('.png')] self.previewImgBin = None if len(previewImgFiles) > 0: # If we found some preview images, use the first one. In fact there is probably # only one try: pf = zf.open(previewImgFiles[0]) self.previewImgBin = pf.read() del pf except zipfile.BadZipfile: pass if not hasattr(self, 'startTime'): # Assume we could not find anything from inside the zipfile, so # fall back to things we can deduce from the filename self.fallbackMetadataFromFilename(zipfilename)
def __init__(self, xmlStr=None, xmlfilename=None, zipfilename=None): """ Take either the name of a zipfile, an XML file, or an XML string, and construct the object from the metadata """ self.previewImgBin = None if xmlStr is None: if xmlfilename is not None: xmlStr = open(xmlfilename).read() elif zipfilename is not None: zf = zipfile.ZipFile(zipfilename, 'r') filenames = [zi.filename for zi in zf.infolist()] safeDirName = [fn for fn in filenames if fn.endswith('.SAFE/')][0] bn = safeDirName.replace('.SAFE/', '') # The meta filename is, rather ridiculously, named something slightly different # inside the SAFE directory, so we have to construct that name. metafilename = bn.replace('PRD', 'MTD').replace('MSIL1C', 'SAFL1C') + ".xml" fullmetafilename = safeDirName + metafilename if fullmetafilename not in filenames: # We have a new format package, in which the meta filename is constant. fullmetafilename = safeDirName + 'MTD_MSIL1C.xml' if fullmetafilename not in filenames: # We have a new format package, in which the meta filename is constant. fullmetafilename = safeDirName + 'MTD_MSIL2A.xml' mf = zf.open(fullmetafilename) xmlStr = mf.read() del mf # Read in the raw content of the preview image png file, and stash on the object previewFilename = bn.replace('PRD', 'BWI') + ".png" previewFullFilename = safeDirName + previewFilename if previewFullFilename not in filenames: # Perhaps we have a new format package, with the preview image as # a jp2 in the QI_DATA directory previewFullFilenameList = [fn for fn in filenames if fnmatch.fnmatch(fn, '*/GRANULE/*/QI_DATA/*PVI.jp2')] if len(previewFullFilenameList) > 0: previewFullFilename = previewFullFilenameList[0] if previewFullFilename in filenames: try: pf = zf.open(previewFullFilename) self.previewImgBin = pf.read() del pf except zipfile.BadZipfile: pass # Read in the whole set of tile-level XML files, too, so we can # grab tileId values from them self.tileNameList = None # This is currently commented out, as it adds significant run-time. I # expect to return to this in future. # tileXmlPattern = safeDirName + "GRANULE/*/*.xml" # tileXmlFiles = [fn for fn in filenames if fnmatch.fnmatch(fn, tileXmlPattern)] # tileIdSet = set() # for tileXml in tileXmlFiles: # tf = zf.open(tileXml) # tileMeta = Sen2TileMeta(fileobj=tf) # del tf # tileIdSet.add(tileMeta.tileId[1:].upper()) # self.tileNameList = sorted(list(tileIdSet)) self.zipfileMetaXML = xmlStr doc = minidom.parseString(xmlStr) generalInfoNode = doc.getElementsByTagName('n1:General_Info')[0] geomInfoNode = doc.getElementsByTagName('n1:Geometric_Info')[0] auxilInfoNode = doc.getElementsByTagName('n1:Auxiliary_Data_Info')[0] qualInfoNode = doc.getElementsByTagName('n1:Quality_Indicators_Info')[0] self.processingLevel = findElementByXPath(generalInfoNode, 'Product_Info/PROCESSING_LEVEL')[0].firstChild.data.strip() self.spacecraftName = findElementByXPath(generalInfoNode, 'Product_Info/Datatake/SPACECRAFT_NAME')[0].firstChild.data.strip() self.satId = "S" + self.spacecraftName.split('-')[1] self.processingSoftwareVersion = findElementByXPath(generalInfoNode, 'Product_Info/PROCESSING_BASELINE')[0].firstChild.data.strip() # The image acquisition start and stop times. In older versions of the ESA processing # software, note that start and stop times were identical (which is obviously wrong) prodStartTimeNode = findElementByXPath(generalInfoNode, 'Product_Info/PRODUCT_START_TIME')[0] prodStartTimeStr = prodStartTimeNode.firstChild.data.strip() self.startTime = datetime.datetime.strptime(prodStartTimeStr, "%Y-%m-%dT%H:%M:%S.%fZ") prodStopTimeNode = findElementByXPath(generalInfoNode, 'Product_Info/PRODUCT_STOP_TIME')[0] prodStopTimeStr = prodStopTimeNode.firstChild.data.strip() self.stopTime = datetime.datetime.strptime(prodStopTimeStr, "%Y-%m-%dT%H:%M:%S.%fZ") # Product generation time, i.e. when ESA processed it generationTimeNode = findElementByXPath(generalInfoNode, 'Product_Info/GENERATION_TIME')[0] generationTimeStr = generationTimeNode.firstChild.data.strip() self.generationTime = datetime.datetime.strptime(generationTimeStr, "%Y-%m-%dT%H:%M:%S.%fZ") relOrbitStr = findElementByXPath(generalInfoNode, 'Product_Info/SENSING_ORBIT_NUMBER')[0].firstChild.data.strip() self.relativeOrbitNumber = int(relOrbitStr) # The cloud indicator cloudPcntNode = findElementByXPath(qualInfoNode, 'Cloud_Coverage_Assessment')[0] self.cloudPcnt = float(cloudPcntNode.firstChild.data.strip()) # The full extPos footprint. This is a very poor excuse for a footprint, but it will # do for now. extPosNode = findElementByXPath(geomInfoNode, 'Product_Footprint/Product_Footprint/Global_Footprint/EXT_POS_LIST')[0] coordsList = [float(v) for v in extPosNode.firstChild.data.strip().split()] x = coordsList[1::2] y = coordsList[0::2] coords = [[x, y] for (x, y) in zip(x, y)] footprintGeom = geomutils.geomFromOutlineCoords(coords) prefEpsg = geomutils.findSensibleProjection(footprintGeom) self.centroidXY = geomutils.findCentroid(footprintGeom, prefEpsg) self.extPosWKT = footprintGeom.ExportToWkt() # Special values in imagery scaleValNodeList = findElementByXPath(generalInfoNode, 'Product_Image_Characteristics/QUANTIFICATION_VALUE') if len(scaleValNodeList) > 0: scaleValNode = scaleValNodeList[0] self.scaleValue = float(scaleValNode.firstChild.data.strip()) else: # We might be in a L2A file, in which case there are several list_scale values for different products scaleValNodeList = findElementByXPath(generalInfoNode, 'Product_Image_Characteristics/QUANTIFICATION_VALUES_LIST') if len(scaleValNodeList) > 0: scaleValNode = scaleValNodeList[0] refScaleNode = findElementByXPath(scaleValNode, 'BOA_QUANTIFICATION_VALUE')[0] self.scaleValue = float(refScaleNode.firstChild.data.strip()) aotScaleNode = findElementByXPath(scaleValNode, 'AOT_QUANTIFICATION_VALUE')[0] self.aotScaleValue = float(aotScaleNode.firstChild.data.strip()) wvpScaleNode = findElementByXPath(scaleValNode, 'WVP_QUANTIFICATION_VALUE')[0] self.wvpScaleValue = float(wvpScaleNode.firstChild.data.strip()) specialValuesNodeList = findElementByXPath(generalInfoNode, 'Product_Image_Characteristics/Special_Values') # These guys have no idea how to use XML properly. Sigh...... for node in specialValuesNodeList: name = node.getElementsByTagName('SPECIAL_VALUE_TEXT')[0].firstChild.data.strip() value = node.getElementsByTagName('SPECIAL_VALUE_INDEX')[0].firstChild.data.strip() if name == "NODATA": self.nullVal = int(value) elif name == "SATURATED": self.saturatedVal = int(value)
def fillInLevel2(self, metaDict): """ Fill in the various fields for a Level-2 product file """ self.productType = metaDict[ '/METADATA/GRANULE_DESCRIPTION/NC_GLOBAL#ProductShortName'] startTimeStr = metaDict['NC_GLOBAL#time_coverage_start'] timeFormat = "%Y-%m-%dT%H:%M:%SZ" # Some products have the trailing Z on the time stamps, some do not. Sigh..... if not startTimeStr.endswith('Z'): timeFormat = timeFormat[:-1] self.startTime = datetime.datetime.strptime(startTimeStr, timeFormat) stopTimeStr = metaDict['NC_GLOBAL#time_coverage_end'] self.stopTime = datetime.datetime.strptime(stopTimeStr, timeFormat) # And the generic time stamp, halfway between start and stop duration = self.stopTime - self.startTime self.datetime = self.startTime + datetime.timedelta(duration.days / 2) self.instrument = metaDict['NC_GLOBAL#sensor'] self.satId = metaDict['NC_GLOBAL#platform'] creationTimeStr = metaDict['NC_GLOBAL#date_created'] self.generationTime = datetime.datetime.strptime( creationTimeStr, timeFormat) self.processingSoftwareVersion = metaDict[ 'NC_GLOBAL#processor_version'] # Leaving this as a string, in case they assume it later. It is a string in # sen2meta. self.processingLevel = metaDict[ '/METADATA/GRANULE_DESCRIPTION/NC_GLOBAL#ProcessLevel'] # Not sure if this is useful, but just in case self.processingMode = metaDict[ '/METADATA/EOP_METADATA/eop:metaDataProperty/eop:processing/NC_GLOBAL#eop:processingMode'] self.absoluteOrbitNumber = int(metaDict['NC_GLOBAL#orbit']) # Make an attempt at the footprint outline. Stole most of this from sen3meta. # Not yet sure whether most S5P products will be swathe products, or if there # will be some which are chopped up further. posListStr = metaDict[ '/METADATA/EOP_METADATA/om:featureOfInterest/eop:multiExtentOf/gml:surfaceMembers/gml:exterior/NC_GLOBAL#gml:posList'] posListStrVals = posListStr.split() numVals = len(posListStrVals) # Note that a gml:posList has pairs in order [lat long ....], with no sensible pair delimiter posListPairs = [ "{} {}".format(posListStrVals[i + 1], posListStrVals[i]) for i in range(0, numVals, 2) ] posListVals = [[float(x), float(y)] for (x, y) in [pair.split() for pair in posListPairs]] footprintGeom = geomutils.geomFromOutlineCoords(posListVals) prefEpsg = geomutils.findSensibleProjection(footprintGeom) if prefEpsg is not None: self.centroidXY = geomutils.findCentroid(footprintGeom, prefEpsg) else: self.centroidXY = None self.outlineWKT = footprintGeom.ExportToWkt() # Currently have no mechanism for a preview image self.previewImgBin = None