#!/usr/bin/python3

##--Michael duPont
##--AVWX-Engine : avwx.py
##--Shared METAR settings and functions
##--2015-09-04

# This file contains a series of functions and variables that can be used
# in any project that needs a means of fetching, interpretting, and/or
# translating METAR and TAF weather data.
#
# While the list of functions is rather large, here are the most useful:
#    getMETAR(station)          Returns METAR report for a given station
#    getTAF(station)            Returns TAF report for a given station
#    parseMETAR(txt)            Returns a key-value dict where 'txt' is the report getMETAR returns
#    parseTAF(txt)              Returns a key-value dict where 'txt' is the report getTAF returns
#    translateMETAR(wxData)     Returns a key-value dict of field translations where 'wxData' is what parseMETAR returns
#    translateTAF(wxData)       Returns a key-value dict of field translations where 'wxData' is what parseTAF returns
#    getInfoForStation(station) Returns a key-value dict of basic info for a given 'station'
#    getFlightRules(vis , splitCloud)
#         Returns int corresponding to flight rules index in flightRules
#         Standard usage states that the current rule is IFR if visibility value is not available
#         vis is string of visibility distance value
#         splitCloud is the ceiling is the format [type,height,modifier]
#         Typical usage:     flightRules[getFlightRules(parsedReport['Visibility'] , getCeiling(parsedReport['Cloud-List']))]
#         Usage with values: flightRules[getFlightRules('8' , ['BKN','014',''])] -> 'MVFR'
#
# Both METAR and TAF reports come in the Standard/International variant and
# the North American variant. parseMETAR and parseTAF automatically figure out
# which parser variant to use, but you're welcome to use the variant functions
# directly if you want. Just know that parsing errors will arise if the
# International parser tries to parse a US report.
#
# Example usage for both METAR and TAF can be found at the bottom of the file.
# You can run this test code by running this file: python avwx.py

import sqlite3 , json , sys
if sys.version_info[0] == 2: import urllib2
elif sys.version_info[0] == 3: from urllib.request import urlopen
from itertools import permutations
from time import strftime
from copy import copy
from os import path
from .xmltodict import parse

##--Logic Vars
flightRules = ['VFR','MVFR','IFR','LIFR']
cloudList = ['FEW','SCT','BKN','OVC']
wxReplacements = {
'RA':'Rain','TS':'Thunderstorm','SH':'Showers','DZ':'Drizzle','VC':'Vicinity','UP':'Unknown Precip',
'SN':'Snow','FZ':'Freezing','SG':'Snow Grains','IC':'Ice Crystals','PL':'Ice Pellets','GR':'Hail','GS':'Small Hail',
'FG':'Fog','BR':'Mist','HZ':'Haze','VA':'Volcanic Ash','DU':'Wide Dust','FU':'Smoke','SA':'Sand','SY':'Spray',
'SQ':'Squall','PO':'Dust Whirls','DS':'Duststorm','SS':'Sandstorm','FC':'Funnel Cloud',
'BL':'Blowing','MI':'Shallow','BC':'Patchy','PR':'Partial','UP':'Unknown'}

metarRMKStarts = [' BLU',' BLU+',' WHT',' GRN',' YLO',' AMB',' RED',' BECMG',' TEMPO',' INTER',' NOSIG',' RMK',' WIND',' QFE',' QFF',' INFO',' RWY',' CHECK']
tafRMKStarts = ['RMK ','AUTOMATED ','COR ','AMD ','LAST ','FCST ','CANCEL ','CHECK ','WND ','MOD ',' BY',' QFE',' QFF']
tafNewLineStarts = [' INTER ' , ' FM' , ' BECMG ' , ' TEMPO ']

##--Station Location Identifiers
RegionsUsingUSParser = ['C', 'K', 'P', 'T']
RegionsUsingInternationalParser = ['A', 'B', 'D', 'E', 'F', 'G', 'H', 'L', 'N', 'O', 'R', 'S', 'U', 'V', 'W', 'Y', 'Z']
#The Central American region is split. Therefore we need to use the first two letters
MStationsUsingUSParser = ['MB', 'MM', 'MT', 'MY']
MStationsUsingInternationalParser = ['MD' , 'MG', 'MH', 'MK' , 'MN', 'MP', 'MR', 'MS', 'MU' , 'MW' , 'MZ']

naUnits = {'Wind-Speed':'kt','Visibility':'sm','Altitude':'ft','Temperature':'C','Altimeter':'inHg'}
inUnits = {'Wind-Speed':'kt','Visibility':'m','Altitude':'ft','Temperature':'C','Altimeter':'hPa'}
curUnits = {} #Global placeholder for report units

#LOCAL_PATH = path.dirname(path.realpath(__file__))
stationDBPath = path.dirname(path.realpath(__file__))+'/stations.sqlite' #Path to the station info database
requestURL = """https://aviationweather.gov/adds/dataserver_current/httpparam?dataSource={0}s&requestType=retrieve&format=XML&stationString={1}&hoursBeforeNow=2"""

####################################################################################################################################
##--Shared Functions

#Returns the index of the earliest occurence of an item from a list in a string
def findFirstInList(txt,aList):
	startIndex = len(txt)+1
	for item in aList:
		if -1 < txt.find(item) < startIndex: startIndex = txt.find(item)
	if -1 < startIndex < len(txt)+1: return startIndex
	return -1

#Remove remarks and split
#Remarks can include RMK and on, NOSIG and on, and BECMG and on
def __getRemarks(txt):
	txt = txt.replace('?' , '').strip(' ')
	#First look for Altimeter in txt
	altIndex = len(txt)+1
	for item in [' A2',' A3',' Q1',' Q0',' Q9']:
		index = txt.find(item)
		if -1 < index < len(txt)-6 and txt[index+2:index+6].isdigit(): altIndex = index
	#Then look for earliest remarks 'signifier'
	sigIndex = findFirstInList(txt , metarRMKStarts)
	if sigIndex == -1: sigIndex = len(txt)+1
	if -1 < altIndex < sigIndex: return txt[:altIndex+6].strip().split(' ') , txt[altIndex+7:]
	elif -1 < sigIndex < altIndex: return txt[:sigIndex].strip().split(' ') , txt[sigIndex+1:]
	return txt.strip().split(' ') , ''

#Provides sanitization for operations that work better when the report is a string
#Returns the first pass sanitized report string
strReplacements = {' C A V O K ':' CAVOK ' , '?':' '}
def __sanitizeFirstPass(txt):
	if len(txt) > 4:
		stid = txt[:4]
		txt = txt[4:] #Prevent changes to station ID
	for key in strReplacements: txt = txt.replace(key , strReplacements[key])
	#Check for missing spaces in front of cloud layers
	#Ex: TSFEW004SCT012FEW///CBBKN080
	for cloud in cloudList:
		if txt.find(cloud) != -1 and txt.find(' '+cloud) == -1:
			startIndex = 0
			counter = 0
			while txt.count(cloud) != txt.count(' '+cloud):
				if len(txt[txt[startIndex:].find(cloud)+startIndex:]) >= 3:
					targetText = txt[txt[startIndex:].find(cloud)+len(cloud)+startIndex:txt[startIndex:].find(cloud)+len(cloud)+startIndex+3]
					if targetText.isdigit() or not targetText.strip('/'):
						txt = txt[:txt[startIndex:].find(cloud)+startIndex] + ' ' + txt[txt[startIndex:].find(cloud)+startIndex:]
				startIndex = startIndex + txt[startIndex:].find(cloud) + len(cloud) + 1
				#Prevent infinite loops
				if counter > txt.count(cloud): break
				counter += 1
	return stid + txt

#Return True if a space shouldn't exist between two items
#This list grew so large that it had to be moved to its own function for readability
def extraSpaceExists(s1 , s2):
	if s1.isdigit():
		# 10 SM
		if s2 in ['SM','0SM']: return True
		# 12 /10
		if len(s2) > 2 and s2[0] == '/' and s2[1:].isdigit(): return True
	if s2.isdigit():
		# OVC 040
		if s1 in cloudList: return True
		# 12/ 10
		if len(s1) > 2 and s1.endswith('/') and s1[:len(s1)-1].isdigit(): return True
		# 12/1 0
		if len(s2) == 1 and len(s1) > 3 and s1[:2].isdigit() and s1.find('/') != -1 and s1[3:].isdigit(): return True
		# Q 1001
		if s1 in ['Q','A']: return True
	# 36010G20 KT
	if s2 == 'KT' and (s1[:5].isdigit() or (s1.startswith('VRB') and s1[3:5].isdigit())) and s1[len(s1)-1].isdigit(): return True
	# 36010K T
	if s2 == 'T' and len(s1) == 6 and (s1[:5].isdigit() or (s1.startswith('VRB') and s1[3:5].isdigit())) and s1[5] == 'K': return True
	# OVC022 CB
	if s2 in cloudTranslationStrings and s2 not in cloudList and len(s1) >= 3 and s1[:3] in cloudList: return True
	# FM 122400
	if s1 in ['FM','TL'] and (s2.isdigit() or (s2.endswith('Z') and s2[:len(s2)-1].isdigit())): return True
	# TX 20/10
	if s1 in ['TX','TN'] and s2.find('/') != -1: return True
	return False

#Sanitize wxData
#We can remove and identify "one-off" elements and fix other issues before parsing a line
#We also return the runway visibility and wind shear since they are very easy to recognize
#and their location in the report is non-standard
itemRemoval = ['AUTO' , 'COR' , 'NSC' , 'NCD' , '$' , 'KT' , 'M' , '.' , 'RTD' , 'SPECI' , 'METAR' , 'CORR']
itemReplacements = {'CALM': '00000KT'}
visPermutations = [''.join(p) for p in permutations('P6SM')]
visPermutations.pop(visPermutations.index('6MPS'))
def __sanitize(wxData , removeCLRandSKC=True):
	shear = ''
	runwayVisibility = []
	for i , item in reversed(list(enumerate(wxData))):
		#print(i , item)
		#Remove elements containing only '/'
		if not item.strip('/'):
			wxData.pop(i)
		#Identify Runway Visibility
		elif len(item) > 4 and item[0] == 'R' and (item[3] == '/' \
		or item[4] == '/') and item[1:3].isdigit():
			runwayVisibility.append(wxData.pop(i))
		#Remove RE from wx codes, REVCTS -> VCTS
		elif len(item) in [4,6] and item.startswith('RE'):
			wxData.pop(i)
		#Fix a slew of easily identifiable conditions where a space does not belong
		elif i and extraSpaceExists(wxData[i-1] , item):
			wxData[i-1] += wxData.pop(i)
		#Remove spurious elements
		elif item in itemRemoval:
			wxData.pop(i)
		#Remove 'Sky Clear' from METAR but not TAF
		elif removeCLRandSKC and item in ['CLR' , 'SKC']:
			wxData.pop(i)
		#Replace certain items
		elif item in itemReplacements:
			wxData[i] = itemReplacements[item]
		#Remove ammend signifier from start of report ('CCA','CCB',etc)
		elif len(item) == 3 and item.startswith('CC') and item[2].isalpha():
			wxData.pop(i)
		#Identify Wind Shear
		elif len(item) > 6 and item.startswith('WS') and item.find('/') != -1:
			shear = wxData.pop(i).replace('KT' , '')
		#Fix inconsistant 'P6SM' Ex: TP6SM or 6PSM -> P6SM
		elif len(item) > 3 and item[len(item)-4:] in visPermutations:
			wxData[i] = 'P6SM'
		#Fix wind T
		elif (len(item) == 6 and item[5] in ['K','T'] and (item[:5].isdigit() or item.startswith('VRB'))) \
		or (len(item) == 9 and item[8] in ['K','T'] and item[5] == 'G' and (item[:5].isdigit() or item.startswith('VRB'))):
			wxData[i] = item[:len(item)-1] + 'KT'
		#Fix joined TX-TN
		elif len(item) > 16 and len(item.split('/')) == 3:
			if item.startswith('TX') and item.find('TN') != -1:
				wxData.insert(i+1 , item[:item.find('TN')])
				wxData[i] = item[item.find('TN'):]
			elif item.startswith('TN') and item.find('TX') != -1:
				wxData.insert(i+1 , item[:item.find('TX')])
				wxData[i] = item[item.find('TX'):]
		#print(i , item)
	return wxData , runwayVisibility , shear

#Altimeter
def __getAltimeterUS(wxData):
	altimeter = ''
	#Get altimeter
	if wxData:
		if wxData[len(wxData)-1][0] == 'A': altimeter = wxData.pop()[1:]
		elif wxData[len(wxData)-1][0] == 'Q':
			global curUnits
			curUnits['Altimeter'] = 'hPa'
			altimeter = wxData.pop()[1:].lstrip('.')
		elif len(wxData[len(wxData)-1]) == 4 and wxData[len(wxData)-1].isdigit(): altimeter = wxData.pop()
	#Some stations report both, but we only need one
	if wxData and (wxData[len(wxData)-1][0] == 'A' or wxData[len(wxData)-1][0] == 'Q'): wxData.pop()
	return wxData , altimeter

def __getAltimeterInternational(wxData):
	altimeter = ''
	#Get altimeter
	if wxData:
		if wxData[len(wxData)-1][0] == 'Q':
			altimeter = wxData.pop()[1:].lstrip('.')
			if altimeter.find('/') != -1: altimeter = altimeter[:altimeter.find('/')]
		elif wxData[len(wxData)-1][0] == 'A':
			global curUnits
			curUnits['Altimeter'] = 'inHg'
			altimeter = wxData.pop()[1:]
	#Some stations report both, but we only need one
	if wxData and (wxData[len(wxData)-1][0] == 'A' or wxData[len(wxData)-1][0] == 'Q'): wxData.pop()
	return wxData , altimeter

def __getTAFAltIceTurb(wxData):
	altimeter = ''
	icing , turbulence = [] , []
	for i in reversed(range(len(wxData))):
		if len(wxData[i]) > 6 and wxData[i].startswith('QNH') and wxData[i][3:7].isdigit():
			altimeter = wxData.pop(i)[3:7]
		elif wxData[i].isdigit():
			if wxData[i][0] == '6': icing.append(wxData.pop(i))
			elif wxData[i][0] == '5': turbulence.append(wxData.pop(i))
	return wxData , altimeter , icing , turbulence

def isPossibleTemp(temp):
	for index in temp:
		if index not in ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'M']: return False
	return True

#Temp/Dewpoint
def __getTempAndDewpoint(wxData):
	for i in reversed(range(len(wxData))):
		if wxData[i].find('/') != -1:
			curVal = copy(wxData[i])
			#///07
			if curVal[0] == '/': curVal = '/' + curVal.strip('/')
			#07///
			elif curVal[len(curVal)-1] == '/': curVal = curVal.strip('/') + '/'
			TD = curVal.split('/')
			if len(TD) != 2: continue
			isValid = True
			for j in range(len(TD)):
				if TD[j] in ['MM','XX']: TD[j] = ''
				elif not isPossibleTemp(TD[j]):
					isValid = False
					break
			if isValid:
				wxData.pop(i)
				return wxData , TD[0] , TD[1]
	return wxData , '' , ''

#Station and Time
def __getStationAndTime(wxData):
	station = wxData.pop(0)
	if wxData and wxData[0].endswith('Z') and wxData[0][:len(wxData[0])-1].isdigit(): rTime = wxData.pop(0)
	elif wxData and len(wxData[0]) == 6 and wxData[0].isdigit(): rTime = wxData.pop(0) + 'Z'
	else: rTime = ''
	return wxData , station , rTime

#Surface wind
def __getWindInfo(wxData):
	direction , speed , gust = '' , '' , ''
	variable = []
	global curUnits
	if wxData:
		item = copy(wxData[0])
		for rep in ['(E)']: item = item.replace(rep , '')
		item = item.replace('O' , '0')
		#09010KT , 09010G15KT
		if item.endswith('KT') \
		or item.endswith('KTS') \
		or item.endswith('MPS') \
		or item.endswith('KMH') \
		or ((len(item) == 5 or (len(item) >= 8 and item.find('G') != -1) and item.find('/') == -1) and (item[:5].isdigit() or (item.startswith('VRB') and item[3:5].isdigit()))):
			#In order of frequency
			if item.endswith('KT'): item = item.replace('KT' , '')
			elif item.endswith('KTS'): item = item.replace('KTS' , '')
			elif item.endswith('MPS'):
				curUnits['Wind-Speed'] = 'm/s'
				item = item.replace('MPS' , '')
			elif item.endswith('KMH'):
				curUnits['Wind-Speed'] = 'km/h'
				item = item.replace('KMH' , '')
			direction = item[:3]
			if item.find('G') != -1:
				gust = item[item.find('G')+1:]
				speed = item[3:item.find('G')]
			else: speed = item[3:]
			wxData.pop(0)
		#elif len(item) > 5 and item[3] == '/' and item[:3].isdigit() and item[4:6].isdigit():
			#direction = item[:3]
			#if item.find('G') != -1:
				#print('Found second G: {0}'.format(item))
				#gIndex = item.find('G')
				#gust = item[gIndex+1:gIndex+3]
				#speed = item[4:item.find('G')]
			#else:
				#speed = item[4:]
			#wxData.pop(0)
	#Separated Gust
	if wxData and 1 < len(wxData[0]) < 4 and wxData[0][0] == 'G' and wxData[0][1:].isdigit():
		gust = wxData.pop(0)[1:]
	#Variable Wind Direction
	if wxData and len(wxData[0]) == 7 and wxData[0][:3].isdigit() and wxData[0][3] == 'V' and wxData[0][4:].isdigit():
		variable = wxData.pop(0).split('V')
	return wxData , direction , speed , gust , variable

#Visibility
def __getVisibility(wxData):
	visibility = ''
	global curUnits
	if wxData:
		item = copy(wxData[0])
		#Vis reported in statue miles
		if item.endswith('SM'):   #10SM
			if item == 'P6SM': visibility = 'P6'
			elif item == 'M1/4SM': visibility = 'M1/4'
			elif item.find('/') == -1: visibility = str(int(item[:item.find('SM')]))  #str(int()) fixes 01SM
			else: visibility = item[:item.find('SM')] #1/2SM
			wxData.pop(0)
			curUnits['Visibility'] = 'sm'
		#Vis reported in meters
		elif len(item) == 4 and item.isdigit():
			visibility = wxData.pop(0)
			curUnits['Visibility'] = 'm'
		elif 5 <= len(item) <= 7 and item[:4].isdigit() and (item[4] in ['M','N','S','E','W'] or item[4:] == 'NDV'):
			visibility = wxData.pop(0)[:4]
			curUnits['Visibility'] = 'm'
		elif len(item) == 5 and item[1:5].isdigit() and item[0] in ['M','P','B']:
			visibility = wxData.pop(0)[1:5]
			curUnits['Visibility'] = 'm'
		elif item.endswith('KM') and item[:item.find('KM')].isdigit():
			visibility = item[:item.find('KM')] + '000'
			wxData.pop(0)
			curUnits['Visibility'] = 'm'
		#Vis statute miles but split
		elif len(wxData) > 1 and wxData[1].endswith('SM') and wxData[1].find('/') != -1 and item.isdigit():   #2 1/2SM
			vis1 = wxData.pop(0)  #2
			vis2 = wxData.pop(0).replace('SM','')  #1/2
			visibility = str(int(vis1)*int(vis2[2])+int(vis2[0]))+vis2[1:]  #5/2
			curUnits['Visibility'] = 'sm'
	return wxData , visibility

#TAF line report type and start/end times
def __getTypeAndTimes(wxData):
	reportType , startTime , endTime = 'BASE' , '' , ''
	#TEMPO, BECMG, INTER
	if wxData and wxData[0] in ['TEMPO','BECMG','INTER']: reportType = wxData.pop(0)
	#PROB[30,40]
	elif wxData and len(wxData[0]) == 6 and wxData[0].startswith('PROB'): reportType = wxData.pop(0)
	#1200/1306
	if wxData and len(wxData[0]) == 9 and wxData[0][4] == '/' and wxData[0][:4].isdigit() and wxData[0][5:].isdigit():
		times = wxData.pop(0).split('/')
		startTime , endTime = times[0] , times[1]
	#FM120000
	elif wxData and len(wxData[0]) > 7 and wxData[0].startswith('FM'):
		reportType = 'FROM'
		if wxData[0].find('/') != -1 and wxData[0][2:].split('/')[0].isdigit() and wxData[0][2:].split('/')[1].isdigit():
			tSplit = wxData.pop(0)[2:].split('/')
			startTime = tSplit[0]
			endTime = tSplit[1]
		elif wxData[0][2:8].isdigit(): startTime = wxData.pop(0)[2:6]
		#TL120600
		if wxData and len(wxData[0]) > 7 and wxData[0].startswith('TL') and wxData[0][2:8].isdigit(): endTime = wxData.pop(0)[2:6]
	return wxData , reportType , startTime , endTime

#Fix rare cloud layer issues
def sanitizeCloud(cloud):
	if len(cloud) < 4: return cloud
	if not cloud[3].isdigit() and cloud[3] != '/':
		if cloud[3] == 'O': cloud = cloud[:3] + '0' + cloud[4:]  #Bad "O": FEWO03 -> FEW003
		else:  #Move modifiers to end: BKNC015 -> BKN015C
			cloud = cloud[:3] + cloud[4:] + cloud[3]
	return cloud

#Transforms a cloud string into a list of strings: [Type , Height (, Optional Modifier)]
#Returns cloud string list
def splitCloud(cloud, beginsWithVV):
	splitCloud = []
	cloud = sanitizeCloud(cloud)
	if beginsWithVV:
		splitCloud.append(cloud[:2])
		cloud = cloud[2:]
	while len(cloud) >= 3:
		splitCloud.append(cloud[:3])
		cloud = cloud[3:]
	if cloud: splitCloud.append(cloud)
	if len(splitCloud) == 1: splitCloud.append('')
	return splitCloud

#Clouds
def __getClouds(wxData):
	clouds = []
	for i in reversed(range(len(wxData))):
		if wxData[i][:3] in cloudList:
			clouds.append(splitCloud(wxData.pop(i) , False))
		elif wxData[i][:2] == 'VV':
			clouds.append(splitCloud(wxData.pop(i) , True))
	return wxData , sorted(clouds , key=lambda pair: (pair[1],pair[0]))

#Returns int based on current flight rules from parsed METAR data
#0=VFR , 1=MVFR , 2=IFR , 3=LIFR
#Note: Common practice is to report IFR if visibility unavailable
def getFlightRules(vis , splitCloud):
	#Parse visibility
	if vis == '': return 2
	elif vis == 'P6': vis = 10
	elif vis.find('/') != -1:
		if vis[0] == 'M': vis = 0
		else: vis = int(vis.split('/')[0]) / int(vis.split('/')[1])
	elif len(vis) == 4 and vis.isdigit(): vis = int(vis) * 0.000621371  #Convert meters to miles
	else: vis = int(vis)
	#Parse ceiling
	if splitCloud: cld = int(splitCloud[1])
	else: cld = 99
	#Determine flight rules
	if (vis <= 5) or (cld <= 30):
		if (vis < 3) or (cld < 10):
			if (vis < 1) or (cld < 5):
				return 3 #LIFR
			return 2 #IFR
		return 1 #MVFR
	return 0 #VFR

#Returns list of ceiling layer from Cloud-List or None if none found
#Only 'Broken', 'Overcast', and 'Vertical Visibility' are considdered ceilings
#Prevents errors due to lack of cloud information (eg. '' or 'FEW///')
def getCeiling(clouds):
	for cloud in clouds:
		if len(cloud) > 1 and cloud[1].isdigit() and cloud[0] in ['OVC','BKN','VV']:
			return cloud
	return None

def parseRemarks(rmk):
	rmkData = {}
	rmk = rmk.split(' ')
	for item in rmk:
		if len(item) in [9,5] and item[0] == 'T' and item[1:].isdigit():
			if item[1] == '1': rmkData['Temp-Decimal'] = '-' + item[2].replace('0','') + item[3] + '.' + item[4]
			elif item[1] == '0': rmkData['Temp-Decimal'] = item[2].replace('0','') + item[3] + '.' + item[4]
			if len(item) == 9:
				if item[5] == '1': rmkData['Dew-Decimal'] = '-' + item[6].replace('0','') + item[7] + '.' + item[8]
				elif item[5] == '0': rmkData['Dew-Decimal'] = item[6].replace('0','') + item[7] + '.' + item[8]
	return rmkData

#Returns True if the station uses the North American format, False if the International format
def usesNAFormat(station):
	if station[0] in RegionsUsingUSParser: return True
	elif station[0] in RegionsUsingInternationalParser: return False
	elif station[:2] in MStationsUsingUSParser: return True
	elif station[:2] in MStationsUsingInternationalParser: return False

####################################################################################################################################
##--METAR Functions

#Get METAR report for 'station' from www.aviationweather.gov
#Returns METAR report string
#Else returns error int
#0=Bad connection , 1=Station DNE/Server Error
#getMETAR pulls from the ADDS API and is 3x faster than getMETAR2
def getMETAR(station):
	try:
		if sys.version_info[0] == 2:
			response = urllib2.urlopen(requestURL.format('metar' , station))
			xml = response.read()
		elif sys.version_info[0] == 3:
			response = urlopen(requestURL.format('metar' , station))
			xml = response.read().decode('utf-8')
		initDictString = json.dumps(parse(xml))
		for word in ['response' , 'data' , 'METAR' , station]:
			if initDictString.find(word) == -1: return 1
		retDict = json.loads(initDictString)['response']['data']['METAR']
		if type(retDict) == dict: return retDict['raw_text']
		elif type(retDict) == list and len(retDict) >= 1: return retDict[0]['raw_text']
		else: return 1
	except:
		return 0

#getMETAR2 scrapes the report from html
def getMETAR2(station):
	try:
		url = 'http://www.aviationweather.gov/metar/data?ids='+station+'&format=raw&date=0&hours=0'
		if sys.version_info[0] == 2:
			response = urllib2.urlopen(url)
			html = response.read()
		elif sys.version_info[0] == 3:
			response = urlopen(url)
			html = response.read().decode('utf-8')
		if html.find(station+'<') != -1: return 1   #Station does not exist/Database lookup error
		reportStart = html.find('<code>'+station+' ')+6      #Report begins with station iden
		reportEnd = html[reportStart:].find('<')        #Report ends with html bracket
		return html[reportStart:reportStart+reportEnd].replace('\n ','')
	except:
		return 0

#Returns a dictionary of parsed METAR data
#Keys: Station, Time, Wind-Direction, Wind-Speed, Wind-Gust, Wind-Variable-Dir, Visibility, Runway-Vis-List, Altimeter, Temperature, Dewpoint, Cloud-List, Other-List, Remarks, Raw-Report, Units
#Units is dict of identified units of measurement for each field
def parseMETAR(txt):
	if len(txt) < 2: return
	if usesNAFormat(txt[:2]): return parseUSMETAR(txt)
	else: return parseInternationalMETAR(txt)

def parseUSMETAR(txt):
	global curUnits
	curUnits = copy(naUnits)
	retWX = {'Raw-Report':txt}
	txt = __sanitizeFirstPass(txt)
	wxData , retWX['Remarks'] = __getRemarks(txt)
	wxData , retWX['Runway-Vis-List'] , notUsed = __sanitize(wxData)
	wxData , retWX['Station'] , retWX['Time'] = __getStationAndTime(wxData)
	wxData , retWX['Cloud-List'] = __getClouds(wxData)
	wxData , retWX['Wind-Direction'] , retWX['Wind-Speed'] , retWX['Wind-Gust'] , retWX['Wind-Variable-Dir'] = __getWindInfo(wxData)
	wxData , retWX['Altimeter'] = __getAltimeterUS(wxData)
	wxData , retWX['Visibility'] = __getVisibility(wxData)
	retWX['Other-List'] , retWX['Temperature'] , retWX['Dewpoint'] = __getTempAndDewpoint(wxData)
	retWX['Units'] = curUnits
	retWX['Flight-Rules'] = flightRules[getFlightRules(retWX['Visibility'] , getCeiling(retWX['Cloud-List']))]
	retWX['Remarks-Info'] = parseRemarks(retWX['Remarks'])
	return retWX

def parseInternationalMETAR(txt):
	global curUnits
	curUnits = copy(inUnits)
	retWX = {'Raw-Report':txt}
	txt = __sanitizeFirstPass(txt)
	wxData , retWX['Remarks'] = __getRemarks(txt)
	wxData , retWX['Runway-Vis-List'] , notUsed = __sanitize(wxData)
	wxData , retWX['Station'] , retWX['Time'] = __getStationAndTime(wxData)
	if 'CAVOK' not in wxData: wxData , retWX['Cloud-List'] = __getClouds(wxData)
	wxData , retWX['Wind-Direction'] , retWX['Wind-Speed'] , retWX['Wind-Gust'] , retWX['Wind-Variable-Dir'] = __getWindInfo(wxData)
	wxData , retWX['Altimeter'] = __getAltimeterInternational(wxData)
	if 'CAVOK' in wxData:
		retWX['Visibility'] = '9999'
		retWX['Cloud-List'] = []
		wxData.pop(wxData.index('CAVOK'))
	else:
		wxData , retWX['Visibility'] = __getVisibility(wxData)
	wxData , retWX['Temperature'] , retWX['Dewpoint'] = __getTempAndDewpoint(wxData)
	retWX['Other-List'] = wxData #Other weather
	retWX['Units'] = curUnits
	retWX['Flight-Rules'] = flightRules[getFlightRules(retWX['Visibility'] , getCeiling(retWX['Cloud-List']))]
	retWX['Remarks-Info'] = parseRemarks(retWX['Remarks'])
	return retWX

####################################################################################################################################
##--TAF Functions

#Get TAF report for 'station' from www.aviationweather.gov
#Returns TAF report string
#Else returns error int
#0=Bad Connection/Unknown Error , 1=Station DNE/Server Error , 2=Could Not Find Report Start
#getTAF pulls from the ADDS API and is 3x faster than getTAF2
def getTAF(station):
	try:
		if sys.version_info[0] == 2:
			response = urllib2.urlopen(requestURL.format('taf' , station))
			xml = response.read()
		elif sys.version_info[0] == 3:
			response = urlopen(requestURL.format('taf' , station))
			xml = response.read().decode('utf-8')
		initDictString = json.dumps(parse(xml))
		for word in ['response' , 'data' , 'TAF' , station]:
			if initDictString.find(word) == -1: return 1
		retDict = json.loads(initDictString)['response']['data']['TAF']
		if type(retDict) == dict: return retDict['raw_text']
		elif type(retDict) == list and len(retDict) >= 1: return retDict[0]['raw_text']
		else: return 1
	except:
		return 0

#getMETAR2 scrapes the report from html
def getTAF2(station):
	try:
		url = 'http://www.aviationweather.gov/taf/data?ids=' + station + '&format=raw&submit=Get+TAF+data'
		if sys.version_info[0] == 2:
			response = urllib2.urlopen(url)
			html = response.read()
		elif sys.version_info[0] == 3:
			response = urlopen(url)
			html = response.read().decode('utf-8')
		if html.find(station+'<') != -1: return 1                             #Station does not exist/Database lookup error
		reportStart = html.find('<code>TAF ')+6                               #Standard report begins with 'TAF'
		if reportStart == 5: reportStart = html.find('<code>'+station+' ')+6  #US report begins with station iden
		if reportStart == 5: return 2                                         #Beginning of report is non-standard/skewed
		reportEnd = html[reportStart:].find('</code>')                        #Report ends with html bracket
		return html[reportStart:reportStart+reportEnd].replace('\n ','')
	except:
		return 0

#Returns a dictionary of parsed TAF data
#'delim' is the divider between forecast lines. Ex: aviationweather.gov uses '<br/>  '
#Keys: Station, Time, Forecast, Remarks, Min-Temp, Max-Temp, Raw-Report, Units
#Oceania stations also have the following keys: Temp-List, Alt-List
#Forecast is list of report dicts in order of time with the following keys:
#Type , Start-Time, End-Time, Flight-Rules, Wind-Direction, Wind-Speed, Wind-Gust, Wind-Shear, Visibility, Altimeter, Cloud-List, Icing-List, Turb-List, Other-List, Probability, Raw-Line
#Units is dict of identified units of measurement for each field
def parseTAF(txt , delim='<br/>  '):
	retWX = {}
	retWX['Raw-Report'] = txt
	while len(txt) > 3 and txt[:4] in ['TAF ' , 'AMD ' , 'COR ']: txt = txt[4:]
	notUsed , retWX['Station'] , retWX['Time'] = __getStationAndTime(txt[:20].split(' '))
	txt = txt.replace(retWX['Station'] , '')
	txt = txt.replace(retWX['Time'] , '')
	global curUnits
	if usesNAFormat(retWX['Station']):
		isInternational = False
		curUnits = copy(naUnits)
	else:
		isInternational = True
		curUnits = copy(inUnits)
	retWX['Remarks'] = ''
	parsedLines = []
	prob = ''
	lines = txt.strip(' ').split(delim)
	while len(lines) > 0:
		line = lines[0].strip(' ')
		line = __sanitizeLine(line)
		#Remove Remarks from line
		index = findFirstInList(line , tafRMKStarts)
		if index != -1:
			retWX['Remarks'] = line[index:]
			line = line[:index].strip(' ')
		#Separate new lines fixed by sanitizeLine
		index = findFirstInList(line , tafNewLineStarts)
		if index != -1:
			lines.insert(1 , line[index+1:])
			line = line[:index]
		#Add empty PROB to next line data
		rawLine = line
		if len(line) == 6 and line.startswith('PROB'):
			prob = line
			line = ''
		if line:
			if isInternational: parsedLine = parseInternationalTAFLine(line)
			else: parsedLine = parseUSTAFLine(line)
			parsedLine['Probability'] = prob
			parsedLine['Raw-Line'] = rawLine
			prob = ''
			parsedLines.append(parsedLine)
		lines.pop(0)
	if parsedLines:
		parsedLines[len(parsedLines)-1]['Other-List'] , retWX['Max-Temp'] , retWX['Min-Temp'] = getTempMinAndMax(parsedLines[len(parsedLines)-1]['Other-List'])
		if not (retWX['Max-Temp'] or retWX['Min-Temp']): parsedLines[0]['Other-List'] , retWX['Max-Temp'] , retWX['Min-Temp'] = getTempMinAndMax(parsedLines[0]['Other-List'])
		parsedLines = findMissingTAFTimes(parsedLines)
		parsedLines = getTAFFlightRules(parsedLines)
	else:
		retWX['Min-Temp'] = ['','']
		retWX['Max-Temp'] = ['','']
	if retWX['Station'][0] == 'A': parsedLines[len(parsedLines)-1]['Other-List'] , retWX['Alt-List'] , retWX['Temp-List'] = getOceaTandQ(parsedLines[len(parsedLines)-1]['Other-List'])
	retWX['Forecast'] = parsedLines
	retWX['Units'] = curUnits
	return retWX

def parseUSTAFLine(txt):
	global curUnits
	curUnits = naUnits
	retWX = {}
	wxData = txt.split(' ')
	wxData , notUsed , retWX['Wind-Shear'] = __sanitize(wxData , removeCLRandSKC=False)
	wxData , retWX['Type'] , retWX['Start-Time'] , retWX['End-Time'] = __getTypeAndTimes(wxData)
	wxData , retWX['Wind-Direction'] , retWX['Wind-Speed'] , retWX['Wind-Gust'] , notUsed = __getWindInfo(wxData)
	wxData , retWX['Visibility'] = __getVisibility(wxData)
	wxData , retWX['Cloud-List'] = __getClouds(wxData)
	retWX['Other-List'] , retWX['Altimeter'] , retWX['Icing-List'] , retWX['Turb-List'] = __getTAFAltIceTurb(wxData)
	return retWX

def parseInternationalTAFLine(txt):
	global curUnits
	curUnits = inUnits
	retWX = {}
	wxData = txt.split(' ')
	wxData , notUsed , retWX['Wind-Shear'] = __sanitize(wxData , removeCLRandSKC=False)
	wxData , retWX['Type'] , retWX['Start-Time'] , retWX['End-Time'] = __getTypeAndTimes(wxData)
	wxData , retWX['Wind-Direction'] , retWX['Wind-Speed'] , retWX['Wind-Gust'] , notUsed = __getWindInfo(wxData)
	if 'CAVOK' in wxData:
		retWX['Visibility'] = '9999'
		retWX['Cloud-List'] = []
		wxData.pop(wxData.index('CAVOK'))
	else:
		wxData , retWX['Visibility'] = __getVisibility(wxData)
		wxData , retWX['Cloud-List'] = __getClouds(wxData)
	retWX['Other-List'] , retWX['Altimeter'] , retWX['Icing-List'] , retWX['Turb-List'] = __getTAFAltIceTurb(wxData)
	return retWX

#Fixes common mistakes with 'new line' signifiers so that they can be recognized
lineFixes = {'TEMP0':'TEMPO','TEMP O':'TEMPO','TMPO':'TEMPO','TE MPO':'TEMPO','TEMP ':'TEMPO ',' EMPO':' TEMPO','TEMO':'TEMPO','TMPO':'TEMPO','T EMPO':'TEMPO','BECM G':'BECMG','BEMCG':'BECMG','BE CMG':'BECMG','BEMG':'BECMG',' BEC ':' BECMG ','BCEMG':'BECMG','B ECMG':'BECMG'}
def __sanitizeLine(txt):
	for key in lineFixes:
		index = txt.find(key)
		if index > -1: txt = txt[:index] + lineFixes[key] + txt[index+len(key):]
	#Fix when space is missing following new line signifiers
	for item in ['BECMG' , 'TEMPO']:
		if txt.find(item) != -1 and txt.find(item + ' ') == -1:
			insertIndex = txt.find(item)+len(item)
			txt = txt[:insertIndex] + ' ' + txt[insertIndex:]
	return txt

#Pull out Max temp at time and Min temp at time items
def getTempMinAndMax(otherList):
	tempMax , tempMin  = '' , ''
	for i in reversed(range(len(otherList))):
		item = otherList[i]
		if len(item) > 6 and item[0] == 'T' and item.find('/') != -1:
			#TX12/1316Z
			if item[1] == 'X':
				tempMax = item
				otherList.pop(i)
			#TNM03/1404Z
			elif item[1] == 'N':
				tempMin = item
				otherList.pop(i)
			#TM03/1404Z T12/1316Z   -> Will fix TN/TX
			elif item[1] == 'M' or item[1].isdigit():
				if tempMin:
					if int(tempMin[2:tempMin.find('/')].replace('M','-')) > int(item[1:item.find('/')].replace('M','-')):
						tempMax = 'TX' + tempMin[2:]
						tempMin = 'TN' + item[1:]
					else: tempMax = 'TX' + item[1:]
				else: tempMin = 'TN' + item[1:]
				otherList.pop(i)
	return otherList , tempMax , tempMin

#Returns a list of items removed from a given list that are all digits from 'fromIndex' until hitting a non-digit item
def getDigitList(aList , fromIndex):
	retList = []
	aList.pop(fromIndex)
	while len(aList) > fromIndex and aList[fromIndex].isdigit(): retList.append(aList.pop(fromIndex))
	return aList , retList

#Get Temp and Alt list for Oceania TAFs
def getOceaTandQ(otherList):
	tList , qList = [] , []
	if 'T' in otherList: otherList , tList = getDigitList(otherList , otherList.index('T'))
	if 'Q' in otherList: otherList , qList = getDigitList(otherList , otherList.index('Q'))
	return otherList , tList , qList

#Fix any missing time issues (except for error/empty lines)
def findMissingTAFTimes(tafLines):
	lastFMLine = 0
	for i , line in enumerate(tafLines):
		if line['End-Time'] == '' and isNotTempoOrProb(line['Type']):
			lastFMLine = i
			if i < len(tafLines)-1:
				for report in tafLines[i+1:]:
					if isNotTempoOrProb(report['Type']): #Ignore TEMPO and PROB
						line['End-Time'] = report['Start-Time']
						break
	if lastFMLine > 0: tafLines[lastFMLine]['End-Time'] = tafLines[0]['End-Time'] #Special case for final forcast
	return tafLines

#Get flight rules by looking for missing data in prior reports
def getTAFFlightRules(tafLines):
	for i , line in enumerate(tafLines):
		tempVis , tempCloud = line['Visibility'] , line['Cloud-List']
		for report in reversed(tafLines[:i]):
			if isNotTempoOrProb(report['Type']): #Ignore TEMPO and PROB
				if tempVis == '': tempVis = report['Visibility']
				if 'SKC' in report['Other-List'] or 'CLR' in report['Other-List']: tempCloud = 'tempClear'
				elif tempCloud == []: tempCloud = report['Cloud-List']
				if tempVis != '' and tempCloud != []: break
		if tempCloud == 'tempClear': tempCloud = []
		line['Flight-Rules'] = flightRules[getFlightRules(tempVis , getCeiling(tempCloud))]
		#print("Using " + str(tempVis) + ' and ' + str(getCeiling(tempCloud)) + ' gives ' + str(line['Flight-Rules']))
	return tafLines

def isNotTempoOrProb(reportType):
	return reportType != 'TEMPO' and not (len(reportType) == 6 and reportType.startswith('PROB'))

####################################################################################################################################
##--Translation Functions

#Format wind elements into a readable sentence
#Returns the translation string
#Ex: NNE-020 (variable 010 to 040) at 14kt gusting to 20kt
def translateWind(wDir , wSpd , wGst , wVar=[] , unit='kt'):
	ret = ''
	#Wind Direction - Cheat Sheet
	#(360) -- 011/012 -- 033/034 -- (045) -- 056/057 -- 078/079 -- (090)
	#(090) -- 101/102 -- 123/124 -- (135) -- 146/147 -- 168/169 -- (180)
	#(180) -- 191/192 -- 213/214 -- (225) -- 236/237 -- 258/259 -- (270)
	#(270) -- 281/282 -- 303/304 -- (315) -- 326/327 -- 348/349 -- (360)
	if wDir == '000': ret += 'Calm'
	elif wDir.isdigit():
		wDir = int(wDir)
		if 304 <= wDir <= 360 or 0 <= wDir <= 56:
			ret += 'N'
			if 304 <= wDir <= 348:
				if 327 <= wDir <= 348: ret += 'N'
				ret += 'W'
			elif 11 <= wDir <= 56:
				if 11 <= wDir <= 33: ret += 'N'
				ret += 'E'
		elif 124 <= wDir <= 236:
			ret += 'S'
			if 124 <= wDir <= 168:
				if 147 <= wDir <= 168: ret += 'S'
				ret += 'E'
		elif 57 <= wDir <= 123:
			ret += 'E'
			if 57 <= wDir <= 78: ret += 'NE'
			elif 102 <= wDir <= 123: 'SE'
		elif 237 <= wDir <= 303:
			ret += 'W'
			if 237 <= wDir <= 258: ret += 'SW'
			elif 282 <= wDir <= 303: ret += 'NW'
		ret += '-' + str(wDir)
	elif wDir == 'VRB': ret += 'Variable'
	if wVar:
		ret += ' (variable ' + wVar[0] + ' to ' + wVar[1] + ')'
	if wSpd and wSpd != '00':
		ret += ' at ' + wSpd + unit
	if wGst:
		ret += ' gusting to ' + wGst + unit
	return ret

#Formats a visibility element into a string with both km and sm values
#Ex: 8km ( 5sm )
def translateVisibility(vis , unit='m'):
	if vis == 'P6': return 'Greater than 6sm ( >9999m )'
	if vis == 'M1/4': return 'Less than .25sm ( <0400m )'
	if vis.find('/') != -1: vis = float(vis[:vis.find('/')]) / int(vis[vis.find('/')+1:])
	try: float(vis)
	except ValueError: return ''
	if unit == 'm':
		converted = float(vis) * 0.000621371
		converted = str(round(converted , 1)).replace('.0' , '') + 'sm'
		vis = str(round(int(vis)/1000.0 , 1)).replace('.0' , '')
		unit = 'km'
	elif unit == 'sm':
		converted = float(vis) / 0.621371
		converted = str(round(converted , 1)).replace('.0' , '') + 'km'
		vis = str(vis).replace('.0' , '')
	else: return ''
	return vis + unit + ' (' + converted + ')'

#Formats a temperature element into a string with both C and F values
#Used for both Temp and Dew
#Ex: 34C (93F)
def translateTemp(temp , unit='C'):
	temp = temp.replace('M','-')
	try: int(temp)
	except ValueError: return ''
	unit = unit.upper()
	if unit == 'C':
		converted = int(temp) * 1.8 + 32
		converted = str(int(round(converted))) + 'F'
	elif unit == 'F':
		converted = (int(temp) - 32) / 1.8
		converted = str(int(round(converted))) + 'C'
	else: return ''
	return temp + unit + ' (' + converted + ')'

#Formats the altimter element into a string with hPa and inHg values
#Ex: 30.11 inHg (10.20 hPa)
def translateAltimeter(alt , unit='hPa'):
	if alt.isdigit(): 1
	elif not alt.isdigit() and len(alt) == 5 and alt[1:].isdigit(): alt = alt[1:]
	else: return ''
	if unit == 'hPa':
		converted = float(alt) / 33.8638866667
		converted = str(round(converted , 2)) + 'inHg'
	elif unit == 'inHg':
		alt = alt[:2] + '.' + alt[2:]
		converted = float(alt) * 33.8638866667
		converted = str(int(round(converted))) + 'hPa'
	else: return ''
	return alt + unit + ' (' + converted + ')'

#Format cloud list into a readable sentence
#Returns the translation string
#Ex: Scattered clouds at 1100ft, Broken layer at 2200ft (Cumulonimbus), Overcast layer at 3600ft - Reported AGL
cloudTranslationStrings = {
	'OVC':'Overcast layer at {0}{1}',
	'BKN':'Broken layer at {0}{1}',
	'SCT':'Scattered clouds at {0}{1}',
	'FEW':'Few clouds at {0}{1}',
	'VV':'Vertical visibility up to {0}{1}',
	'CLR':'Sky Clear',
	'SKC':'Sky Clear',
	'AC':'Altocumulus',
	'ACC':'Altocumulus Castellanus',
	'AS':'Altostratus',
	'CB':'Cumulonimbus',
	'CC':'Cirrocumulus',
	'CI':'Cirrus',
	'CS':'Cirrostratus',
	'CU':'Cumulus',
	'FC':'Fractocumulus',
	'FS':'Fractostratus',
	'NS':'Nimbostratus',
    'SC':'Stratocumulus',
	'ST':'Stratus',
	'TCU':'Towering Cumulus'
	}
def translateClouds(cloudList , unit='ft'):
	retList = []
	for cloud in cloudList:
		if len(cloud) == 2 and cloud[1].isdigit() and cloud[0] in cloudTranslationStrings: retList.append(cloudTranslationStrings[cloud[0]].format(int(cloud[1])*100 , unit))
		elif len(cloud) == 3 and cloud[1].isdigit() and cloud[0] in cloudTranslationStrings and cloud[2] in cloudTranslationStrings: retList.append((cloudTranslationStrings[cloud[0]]+' ('+cloudTranslationStrings[cloud[2]]+')').format(int(cloud[1])*100 , unit))
	if retList:	return ', '.join(retList) + ' - Reported AGL'
	return 'Sky clear'

#Translates weather codes into readable strings
#Returns translated string of variable length
def translateWX(wx):
	wxString = ''
	if wx[0] == '+':
		wxString = 'Heavy '
		wx = wx[1:]
	elif wx[0] == '-':
		wxString = 'Light '
		wx = wx[1:]
	if len(wx) not in [2,4,6]: return wx  #Return wx if wx is not a code, ex R03/03002V03
	for i in range(len(wx)//2):
		if wx[:2] in wxReplacements: wxString += wxReplacements[wx[:2]] + ' '
		else: wxString += wx[:2]
		wx = wx[2:]
	return wxString.strip(' ')

#Translate the list of wx codes (otherList) into a readable sentence
#Returns the translation string
def translateOtherList(wxList):
	retList = []
	for item in wxList: retList.append(translateWX(item))
	return ', '.join(retList)

#Translate wind shear into a readable string
#Ex: Wind shear 2000ft from 140 at 30kt
def translateWindShear(shear , unitAlt='ft' , unitWnd='kt'):
	if not shear or shear.find('WS') == -1 or shear.find('/') == -1: return ''
	shear = shear[2:].split('/')
	return 'Wind shear ' + str(int(shear[0])*100) + unitAlt + ' from ' + shear[1][:3] + ' at ' + shear[1][3:] + unitWnd

#Translate the list of turbulance or icing into a readable sentence
#Ex: Occasional moderate turbulence in clouds from 3000ft to 14000ft
turbConditions = {'0':'None','1':'Light turbulence','2':'Occasional moderate turbulence in clear air','3':'Frequent moderate turbulence in clear air','4':'Occasional moderate turbulence in clouds','5':'Frequent moderate turbulence in clouds','6':'Occasional severe turbulence in clear air','7':'Frequent severe turbulence in clear air','8':'Occasional severe turbulence in clouds','9':'Frequent severe turbulence in clouds','X':'Extreme turbulence'}
iceConditions = {'0':'No icing','1':'Light icing','2':'Light icing in clouds','3':'Light icing in precipitation','4':'Moderate icing','5':'Moderate icing in clouds','6':'Moderate icing in precipitation','7':'Severe icing','8':'Severe icing in clouds','9':'Severe icing in precipitation'}
def translateTurbIce(aList , unit='ft'):
	if not aList: return ''
	#Determine turbulance or icing
	if aList[0][0] == '5':
		layerType = 'Turbulance'
		conditions = turbConditions
	elif aList[0][0] == '6':
		layerType = 'Icing'
		conditions = iceConditions
	else: return ''
	#Create list of split items (type , floor , height)
	splitList = []
	for item in aList:
		if len(item) == 6: splitList.append([item[1:2],item[2:5],item[5:6]])
	#Combine items that cover a layer greater than 9000ft
	for i in reversed(range(len(splitList)-1)):
		if splitList[i][2] == '9' and splitList[i][0] == splitList[i+1][0] and int(splitList[i+1][1]) == (int(splitList[i][1]) + int(splitList[i][2])*10):
			splitList[i][2] = str(int(splitList[i][2]) + int(splitList[i+1][2]))
			splitList.pop(i+1)
	#Return joined, formatted string from splitList items
	return ', '.join(['{0} from {1}{3} to {2}{3}'.format(conditions[item[0]] , int(item[1])*100 , int(item[1])*100 + int(item[2])*1000 , unit) for item in splitList])

#Format the Min and Max temp elemets into a readable string
#Ex: Maximum temperature of 23C (73F) at 18-15:00Z
def translateMinMaxTemp(temp , unit='C'):
	if not temp or len(temp) < 7: return ''
	if temp[:2] == 'TX': tempType = 'Maximum'
	elif temp[:2] == 'TN': tempType = 'Minimum'
	else: return ''
	temp = temp[2:].replace('M' , '-').replace('Z','').split('/')
	if len(temp[1]) > 2: temp[1] = temp[1][:2] + '-' + temp[1][2:]
	return tempType + ' temperature of ' + translateTemp(temp[0] , unit) + ' at ' + temp[1] + ':00Z'

#Translate Visibility, Altimeter, Clouds, and Other
def translateShared(wxData , units):
	translations = {}
	translations['Visibility'] = translateVisibility(wxData['Visibility'] , units['Visibility'])
	translations['Altimeter'] = translateAltimeter(wxData['Altimeter'] , units['Altimeter'])
	translations['Clouds'] = translateClouds(wxData['Cloud-List'] , units['Altitude'])
	translations['Other'] = translateOtherList(wxData['Other-List'])
	return translations

#Translate the results of parseMETAR
#Keys: Wind, Visibility, Clouds, Temperature, Dewpoint, Altimeter, Other
def translateMETAR(wxData):
	units = wxData['Units']
	translations = translateShared(wxData , units)
	translations['Wind'] = translateWind(wxData['Wind-Direction'] , wxData['Wind-Speed'] , wxData['Wind-Gust'] , wxData['Wind-Variable-Dir'] , units['Wind-Speed'])
	translations['Temperature'] = translateTemp(wxData['Temperature'] , units['Temperature'])
	translations['Dewpoint'] = translateTemp(wxData['Dewpoint'] , units['Temperature'])
	return translations

#Translate the results of parseTAF
#Keys: Forecast, Min-Temp, Max-Temp
#Forecast keys: Wind, Visibility, Clouds, Altimeter, Wind-Shear, Turbulance, Icing, Other
def translateTAF(wxData):
	translations = {'Forecast':[]}
	units = wxData['Units']
	for line in wxData['Forecast']:
		transLine = translateShared(line , units)
		transLine['Wind'] = translateWind(line['Wind-Direction'] , line['Wind-Speed'] , line['Wind-Gust'] , unit=units['Wind-Speed'])
		transLine['Wind-Shear'] = translateWindShear(line['Wind-Shear'] , wxData['Units']['Altitude'] , units['Wind-Speed'])
		transLine['Turbulance'] = translateTurbIce(line['Turb-List'] , units['Altitude'])
		transLine['Icing'] = translateTurbIce(line['Icing-List'] , units['Altitude'])
		translations['Forecast'].append(transLine)
	translations['Min-Temp'] = translateMinMaxTemp(wxData['Min-Temp'] , units['Temperature'])
	translations['Max-Temp'] = translateMinMaxTemp(wxData['Max-Temp'] , units['Temperature'])
	return translations

def createMETARSummary(wxTrans):
	sumList = []
	if 'Wind' in wxTrans and wxTrans['Wind']: sumList.append('Winds ' + wxTrans['Wind'])
	if 'Visibility' in wxTrans and wxTrans['Visibility']: sumList.append('Vis ' + wxTrans['Visibility'][:wxTrans['Visibility'].find(' (')].lower())
	if 'Temperature' in wxTrans and wxTrans['Temperature']: sumList.append('Temp ' + wxTrans['Temperature'][:wxTrans['Temperature'].find(' (')])
	if 'Dewpoint' in wxTrans and wxTrans['Dewpoint']: sumList.append('Dew ' + wxTrans['Dewpoint'][:wxTrans['Dewpoint'].find(' (')])
	if 'Altimeter' in wxTrans and wxTrans['Altimeter']: sumList.append('Alt ' + wxTrans['Altimeter'][:wxTrans['Altimeter'].find(' (')])
	if 'Other' in wxTrans and wxTrans['Other']: sumList.append(wxTrans['Other'])
	if 'Clouds' in wxTrans and wxTrans['Clouds']: sumList.append(wxTrans['Clouds'].replace(' - Reported AGL' , ''))
	return ', '.join(sumList)

def createTAFLineSummary(wxTrans):
	sumList = []
	if 'Wind' in wxTrans and wxTrans['Wind']: sumList.append('Winds ' + wxTrans['Wind'])
	if 'Visibility' in wxTrans and wxTrans['Visibility']: sumList.append('Vis ' + wxTrans['Visibility'][:wxTrans['Visibility'].find(' (')].lower())
	if 'Altimeter' in wxTrans and wxTrans['Altimeter']: sumList.append('Alt ' + wxTrans['Altimeter'][:wxTrans['Altimeter'].find(' (')])
	if 'Other' in wxTrans and wxTrans['Other']: sumList.append(wxTrans['Other'])
	if 'Clouds' in wxTrans and wxTrans['Clouds']: sumList.append(wxTrans['Clouds'].replace(' - Reported AGL' , ''))
	if 'Wind-Shear' in wxTrans and wxTrans['Wind-Shear']: sumList.append(wxTrans['Wind-Shear'])
	if 'Turbulance' in wxTrans and wxTrans['Turbulance']: sumList.append(wxTrans['Turbulance'])
	if 'Icing' in wxTrans and wxTrans['Icing']: sumList.append(wxTrans['Icing'])
	return ', '.join(sumList)

####################################################################################################################################
##--Station data

#Provide basic station info with the keys below
dbHeaders = ['ICAO','Country','State','City','Name','IATA','Elevation','Latitude','Longitude','Priority']
def getInfoForStation(station):
	conn = sqlite3.connect(stationDBPath)
	#conn.text_factory = str
	curs = conn.cursor()
	curs.execute('SELECT '+','.join(dbHeaders)+' FROM Stations WHERE icao=?' , (station,))
	row = curs.fetchone()
	ret = {}
	if row:
		for i in range(len(row)): ret[dbHeaders[i]] = row[i]
	return ret

####################################################################################################################################
##--Example Testing
#These tests provide example usage for the primary public functions

#Adds timestamp to begining of print statement
#Returns string of time + logString
def timestamp(logString): return strftime('%d %H:%M:%S - ') + logString

#Retrive, parse, and display METAR report
def metarTest(station):
	ret = timestamp(station + '\n\n')
	txt = getMETAR(station)
	#txt = 'VTSF 230200Z 22006KT 1701V240 9999 FEW020 30/25 Q1013 A2993 INFO C / RWY 19'
	if type(txt) == int:
		if txt: ret += 'Station does not exist/Database lookup error'
		else: ret += 'http connection error'
	else:
		data = parseMETAR(txt)
		for key in data: ret += '{0}  --  {1}\n'.format(key , data[key])
		ret += 'Flight rules for "{0}" and "{1}"  --  "{2}"'.format(data['Visibility'] , getCeiling(data['Cloud-List']) , flightRules[getFlightRules(data['Visibility'] , getCeiling(data['Cloud-List']))])
		translation = translateMETAR(data)
		ret += '\n\nTranslation'
		for key in translation: ret += '\n' + key + ':   ' + translation[key]
		ret += '\nSummary: ' + createMETARSummary(translation) + '\n'
		#ret += str(getInfoForStation(station))
	print(ret)

#Retrive, parse, and display TAF report
def tafTest(station):
	ret = timestamp(station + '\n\n')
	txt = getTAF(station)
	#txt = 'TAF OPLA 180345Z 1806/1912 33008KT 4000 HZ NSC TX42/1810Z TN29/1900Z <br/>  PROB30 <br/>  TEMPO 1810/1812 32015G25KT 3000 DRDU FM 181500 TL 190100 25003KT 4000 HZ NSC <br/>  TEMPO 1901/1903 30004KT 2500 FU'
	if type(txt) == int:
		if txt: ret += 'Station does not exist/Database lookup error'
		else: ret += 'http connection error'
	else:
		delim = '<br/>  '
		ret = timestamp(station + '\n\n') + txt + '\n\n'
		taf = parseTAF(txt , delim)
		#Print report lines
		for line in txt.strip(' ').split(delim): ret += line + '\n'
		#Print header data
		ret += '\n' + taf['Station'] + '\n' + taf['Time'] + '\n' + taf['Remarks'] + '\n' + str(taf['Units']) + '\n' + str(taf['Min-Temp']) + '\n' + str(taf['Max-Temp']) + '\n\n'
		#Print Forecasts' start and end times
		for line in taf['Forecast']:
			ret += line['Start-Time'] + ' - ' + line['End-Time'] + '\n'
		ret += '\n'
		#Print Forecast dicts
		for lineDict in taf['Forecast']: ret += str(lineDict) + '\n\n'
		#Print Translation
		trans = translateTAF(taf)
		ret += 'Translation\n\n' + str(trans) + '\n\nSummary:'
		for line in trans['Forecast']: ret += '\n' + createTAFLineSummary(line)
	print(ret)

if __name__ == '__main__':
	station = 'CWER'
	#print(getInfoForStation(station))
	metarTest(station)
	print('\n------------------------------------------\n')
	#tafTest(station)


from __future__ import absolute_import, division, print_function
# LIBTBX_SET_DISPATCHER_NAME phenix.map_box

import mmtbx.utils
import mmtbx.model
from mmtbx.refinement import print_statistics
import iotbx.pdb
import libtbx.phil
from libtbx.utils import Sorry
import os, sys
from iotbx import reflection_file_utils
from iotbx.file_reader import any_file
from cctbx import maptbx
from scitbx.matrix import col
from six.moves import zip

master_phil = libtbx.phil.parse("""
  include scope libtbx.phil.interface.tracking_params
  pdb_file = None
    .type = path
    .help = Optional model file used to define region to be cut out
    .short_caption = Model file (optional)
    .style = file_type:pdb bold input_file
  map_coefficients_file = None
    .type = path
    .help = Input map coefficients file (alternative to ccp4 map file)
    .short_caption = Map coefficients
    .style = file_type:hkl bold input_file process_hkl child:map_labels:label
  label = None
    .type = str
    .short_caption = Map labels
    .help = Labels for map coefficients file
    .style = renderer:draw_map_arrays_widget parent:file_name:map_coefficients_file
  ccp4_map_file = None
    .help = Input map file (CCP4/mrc format).
    .short_caption = Input map file
    .type = str
  target_ncs_au_file = None
    .help = File with model indicating which au to choose in extract_unique
    .short_caption = Input target ncs au file
    .type = str
  half_map_list = None
    .type = strings
    .help = Half maps (extract_unique only). Supply file names \
              separated by spaces
    .short_caption = Half maps (extract_unique only)
  selection = all
    .type = str
    .help = Atom selection to be applied to input PDB file
    .short_caption = Atom selection (optional)
    .input_size = 400
  selection_radius = 3.0
    .type = float
    .help = Atoms within selection_radius of a selected atom model will be\
             kept as part of the selection.
    .short_caption = Selection radius
  box_cushion = 3.0
    .type = float
    .help = If mask_atoms is False, a box of density will be cut out around\
            the input model (after selections are applied to the model). \
            The size of the box of density will be box_cushion bigger than \
            the model.  Box cushion also applied if density_select is set.
    .short_caption = Box cushion

  mask_atoms=False
    .type=bool
    .help = Set map values to 0 outside molecular mask
    .short_caption = Mask atoms

  mask_atoms_atom_radius = 3.
    .type=float
     .help = Radius for masking around atoms
     .short_caption = Mask atoms atom radius

  value_outside_atoms = None
    .type = str
    .help = Set to 'mean' to make average value same inside and outside mask.
    .short_caption = Value outside atoms
  resolution_factor = 1./4
    .type = float
    .help = Resolution factor for calculation of map coefficients
    .short_caption = Resolution factor
  map_scale_factor = None
    .type = float
    .help = Scale factor to apply to map
    .short_caption = Map scale factor
  scale_max = 99999
    .type = float
    .help = Maximum value of amplitudes for output mtz file. If None, apply\
             volume scaling
    .short_caption = Scale max
  resolution = None
    .type = float
    .help = Resolution for calculation of output map coefficients. Default is \
            based on the gridding of the map (and may be higher-resolution than\
            you want).
    .short_caption = Resolution
  output_format = xplor *mtz *ccp4
    .type=choice(multi=True)
    .help = Output format(s) for boxed map. Note that mtz format is only\
            available if keep_origin=False or keep_map_size=True. (These \
            are the cases where the map is cut down to size and placed \
            at the origin or there is a full unit cell of data.)
    .short_caption = Output format

  output_file_name_prefix=None
    .type = str
    .help = Prefix for output file names. Default is name of the pdb file \
            without the ".pdb" suffix.
    .short_caption = Output file name prefix

  mask_select = False
    .type = bool
    .help = Select boundaries (min,max in x,y,z) based on auto-mask
    .short_caption = Mask select

  density_select = False
    .type = bool
    .help = Select boundaries based on where density is located.
    .short_caption = Density select

  density_select_threshold = 0.05
    .type = float
    .help = Choose region where density is this fraction of maximum or greater
    .short_caption = density_select threshold

  get_half_height_width = True
    .type = bool
    .help = Use 4 times half-width at half-height as estimate of max size \
              in density_select
    .short_caption = Use half-height width

  symmetry = None
    .type = str
    .help = Optional symmetry (e.g., D7, I, C2) to be used if extract_unique\
            is set.  Alternative to symmetry_file.  To find symmetry \
            automatically specify symmetry=ALL.
    .short_caption = Symmetry
  symmetry_file = None
    .type = path
    .help = Symmetry file.\
            Symmetry or symmetry_file required if extract_unique=True.  \
            May be a \
            Phenix .ncs_spec file or BIOMTR records or a resolve ncs file.
    .short_caption = Symmetry file

  sequence_file = None
    .type = path
    .help = Sequence file (any standard format). Can be unique part or \
            all copies.  Used in identification of unique part of map \
            and in masking with mask_select
    .short_caption = Sequence file (optional)

  molecular_mass = None
    .type = float
    .help = Molecular mass of object in map in Da (i.e., 33000 for 33 Kd).\
              Used in identification \
            of unique part of map and in masking by mask_select.
    .short_caption = Molecular mass (optional)

  solvent_content = None
    .type = float
    .help = Optional fraction of volume of map that is empty.  \
            Used in identification \
            of unique part of map and in masking by mask_select
    .short_caption = Solvent content

  extract_unique = False
    .type = bool
    .help = Extract unique part of map. Requires either symmetry_file or \
            symmetry and\
            either sequence file or molecular mass to be supplied. If chain \
            type is not protein it should be set as well.
    .short_caption = Extract unique

  box_buffer = 5
    .type = int
    .help = Padding around unique region in extract_unique
    .short_caption = Padding around unique region

  soft_mask_extract_unique = True
    .type = bool
    .help = Create soft mask at edges of extract_unique box (feather map into \
               edge of box). Uses resolution as mask_radius
      .short_caption = Soft mask in extract unique

  mask_expand_ratio = 1
      .type = int
      .help = Mask expansion relative to resolution for extract_unique
      .short_caption = Mask expand ratio

  regions_to_keep = None
    .type = int
    .short_caption = Regions to keep
    .help = You can specify a limit to the number of regions to keep\
             when generating the asymmetric unit of density.

  keep_low_density = True
    .type = bool
    .help = Get remainder (weak density) with extract_unique.
    .short_caption = Get remainder

  chain_type = None *PROTEIN DNA RNA
    .type = choice
    .help = Chain type. Only used if extract_unique is set. Has minor effect \
            in setting thresholds for identification of molecular region.\
            Use None if there is a mixture.
    .short_caption = Chain type

  soft_mask = False
    .type=bool
    .help = Use Gaussian mask in mask_atoms and on outside surface of box
    .short_caption = Soft mask

  soft_mask_radius=3
    .type=float
    .help = Gaussian mask smoothing radius
    .short_caption = Soft mask radius

  lower_bounds = None
    .type = ints
    .help = Lower bounds for cut out box. You can specify them directly.\
            NOTE: lower and upper bounds refer to grid points after shifting \
            the map to place the origin at (0,0,0). To refer to absolute \
            values specify bounds_are_absolute=True.
    .short_caption = Lower bounds

  upper_bounds = None
    .type = ints
    .help = Upper bounds for cut out box.  You can specify them directly.\
            NOTE: lower and upper bounds refer to grid points after shifting \
            the map to place the origin at (0,0,0). To refer to absolute \
            values specify bounds_are_absolute=True.
    .short_caption = Upper bounds

  bounds_are_absolute = False
    .type = bool
    .help = Define lower and upper bounds as absolute. \
            NOTE: lower and upper bounds refer to grid points after shifting \
            the map to place the origin at (0,0,0). To refer to absolute \
            values specify bounds_are_absolute=True.
    .short_caption = Bounds are absolute

  zero_outside_original_map = False
    .type = bool
    .help = If bounds for new map are outside original map, zero all points\
             outside of original map
    .short_caption = Zero outside original map
  keep_map_size = False
    .type=bool
    .help = Keep original map gridding (do not cut anything out). \
            Use to apply soft_mask and/or mask_atoms keeping same map size.
    .short_caption = Keep map size

  keep_origin = True
    .type=bool
    .help = Write out map, map_coefficients, and model \
            with origin in original location.  \
            If False, shift the origin to (0,0,0).  \
            NOTE: to cut out a part of a map, shift the origin to (0,0,0),\
               and make a new small map use keep_origin=False\
               keep_input_unit_cell_and_grid=False
    .short_caption = Keep origin

  keep_input_unit_cell_and_grid = True
     .type = bool
     .help = Keep the input unit_cell dimensions and unit_cell_grid. \
             If False, use the dimensions and grid of the cut out box as the \
              unit cell map_box dimensions and grid.\
            NOTE: to cut out a part of a map, shift the origin to (0,0,0),\
               and make a new small map set keep_origin=False and \
               keep_input_unit_cell_and_grid=False
     .short_caption = Keep input unit cell and grid

  output_unit_cell = None
     .type = floats
     .help = You can specify the unit cell for your map with 3 numbers. \
              This should normally\
             not be necessary. It can be used to fix a map that has the \
             wrong unit cell.
     .short_caption = Output unit cell
     .expert_level = 3

  output_unit_cell_grid = None
    .type = ints
    .help = You can specify the grid (3 integers) corresponding to the \
              output unit cell. \
              This can be used to specify the full grid for the unit cell. \
              if output_unit_cell is not specified, new unit cell parameters\
              will be generated to maintain the grid spacing.
    .short_caption = Output unit cell grid
    .expert_level = 3

  output_origin_grid_units = None
    .type = ints
    .help = You can specify the origin of your output map.  Normally you \
           should use keep_origin=True or False to specify your origin \
           but if you want to move it to a specific grid point you can do that.\
    .short_caption = Output origin
    .expert_level = 3

  output_origin_match_this_file = None
    .type = path
    .help = As output_origin_grid_units, but use origin from this file
    .short_caption = File with origin info

  bounds_match_this_file = None
    .type = path
    .help = Take the lower and upper bounds from this map file and apply them \
             to the input map file.
    .short_caption = File with bounds to match

  output_external_origin = None
    .type = floats
    .help = Write ORIGIN record to map file (this is an external origin \
            used to specify relationship to external files such as model files.\
            Three floating point numbers (A).
    .short_caption = output external origin

  restrict_map_size = False
    .type=bool
    .help = Do not go outside original map boundaries
    .short_caption = Restrict map size

  ignore_symmetry_conflicts = False
    .type=bool
    .help = Ignore unit cell from model if it conflicts with the map.
    .short_caption = Ignore symmetry conflicts

  output_ccp4_map_mean = None
    .type = float
    .help = Choose mean and SD of output CCP4 map
    .short_caption = Mean of output CCP4 map

  output_ccp4_map_sd = None
    .type = float
    .help = Choose mean and SD of output CCP4 map
    .short_caption = SD of output CCP4 map

  output_map_labels = None
    .type = str
    .multiple = True
    .help = Add this label to output map
    .short_caption = Add label

  gui
    .help = "GUI-specific parameter required for output directory"
  {
    output_dir = None
    .type = path
    .style = output_dir
  }
""", process_includes=True)

master_params = master_phil

def remove_element(text_list,element=None):
    new_text_list=[]
    for x in text_list:
      if x != element:
        new_text_list.append(x)
    return new_text_list

def run(args, crystal_symmetry=None,
     ncs_object=None,
     pdb_hierarchy=None,
     map_data=None,
     mask_data=None,
     half_map_data_list=None,
     half_map_labels_list=None,
     lower_bounds=None,
     upper_bounds=None,
     write_output_files=True,
     log=None):
  h = "phenix.map_box: extract box with model and map around selected atoms"
  if(log is None): log = sys.stdout
  print_statistics.make_header(h, out=log)
  default_message="""\

%s.

Usage:
  phenix.map_box model.pdb map_coefficients.mtz selection="chain A and resseq 1:10"

or

  phenix.map_box map.ccp4 density_select=True

Parameters:"""%h
  if(len(args) == 0 and not pdb_hierarchy):
    print(default_message)
    master_phil.show(prefix="  ")
    return

  # Process inputs ignoring symmetry conflicts just to get the value of
  #   ignore_symmetry_conflicts...

  inputs = mmtbx.utils.process_command_line_args(args = args,
      cmd_cs=crystal_symmetry,
      master_params = master_phil,
      suppress_symmetry_related_errors=True)
  params = inputs.params.extract()

  # Now process inputs for real and write a nice error message if necessary.
  try:
    inputs = mmtbx.utils.process_command_line_args(args = args,
      cmd_cs=crystal_symmetry,
      master_params = master_phil,
      suppress_symmetry_related_errors=params.ignore_symmetry_conflicts)
  except Exception as e:
    if str(e).find("symmetry mismatch ")>1:
      raise Sorry(str(e)+"\nTry 'ignore_symmetry_conflicts=True'")
    else:
      raise e

  params = inputs.params.extract()
  master_phil.format(python_object=params).show(out=log)

  # Overwrite params with parameters in call if available
  if lower_bounds:
     params.lower_bounds=lower_bounds
  if upper_bounds:
     params.upper_bounds=upper_bounds

  # PDB file
  if params.pdb_file and not inputs.pdb_file_names and not pdb_hierarchy:
    inputs.pdb_file_names=[params.pdb_file]
  if(len(inputs.pdb_file_names)!=1 and not params.density_select and not
    params.mask_select and not
    pdb_hierarchy and not params.keep_map_size and not params.upper_bounds
     and not params.extract_unique and not params.bounds_match_this_file):
    raise Sorry("PDB file is needed unless extract_unique, "+
      "density_select, mask_select, keep_map_size \nor bounds are set .")
  if (len(inputs.pdb_file_names)!=1 and not pdb_hierarchy and \
       (params.mask_atoms )):
    raise Sorry("PDB file is needed for mask_atoms")
  if params.soft_mask and (not params.resolution) and \
        (len(inputs.pdb_file_names)!=1 and not pdb_hierarchy):
    raise Sorry("Need resolution for soft_mask without PDB file")
  if ((params.density_select or params.mask_select) and params.keep_map_size):
    raise Sorry("Cannot set both density_select/mask_select and keep_map_size")
  if ((params.density_select or params.mask_select) and params.upper_bounds):
    raise Sorry("Cannot set both density_select/mask_select and bounds")
  if (params.keep_map_size and params.upper_bounds):
    raise Sorry("Cannot set both keep_map_size and bounds")
  if (params.upper_bounds and not params.lower_bounds):
    raise Sorry("Please set lower_bounds if you set upper_bounds")
  if (params.extract_unique):
    if (not params.resolution):
      raise Sorry("Please set resolution for extract_unique")
    if (not params.symmetry) and (not params.symmetry_file) and \
        (not ncs_object):
      raise Sorry(
        "Please supply a symmetry file or symmetry for extract_unique (you "+
       "\ncan try symmetry=ALL if you do not know your symmetry or "+
        "symmetry=C1 if \nthere is none)")
      from mmtbx.ncs.ncs import ncs
      ncs_object=ncs()
      ncs_object.set_unit_ncs()

  if params.keep_input_unit_cell_and_grid and (
      (params.output_unit_cell_grid is not None ) or
      (params.output_unit_cell is not None ) ):
    raise Sorry("If you set keep_input_unit_cell_and_grid then you cannot "+\
       "set \noutput_unit_cell_grid or output_unit_cell")

  if (write_output_files) and ("mtz" in params.output_format) and (
       (params.keep_origin) and (not params.keep_map_size)):
    print("\nNOTE: Skipping write of mtz file as keep_origin=True and \n"+\
       "keep_map_size is False\n")
    params.output_format=remove_element(params.output_format,element='mtz')

  if (write_output_files) and ("mtz" in params.output_format) and (
       (params.extract_unique)):
    print("\nNOTE: Skipping write of mtz file as extract_unique=True\n")
    params.output_format=remove_element(params.output_format,element='mtz')

  if params.output_origin_match_this_file or params.bounds_match_this_file:
    if params.output_origin_match_this_file:
      fn=params.output_origin_match_this_file
      if params.bounds_match_this_file:
        raise Sorry("Cannot match origin and bounds at same time")
    else:
      fn=params.bounds_match_this_file
    if not params.ccp4_map_file:
      raise Sorry(
       "Need to specify your input file with ccp4_map_file=xxx if you use "+
        "output_origin_match_this_file=xxxx or bounds_match_this_file=xxxx")

    af = any_file(fn)
    if (af.file_type == 'ccp4_map'):
      origin=af.file_content.data.origin()
      if params.output_origin_match_this_file:
        params.output_origin_grid_units=origin
        print("Origin of (%s,%s,%s) taken from %s" %(
           origin[0],origin[1],origin[2],fn))
      else:
        all=af.file_content.data.all()
        params.lower_bounds=origin
        print("Lower bounds of (%s,%s,%s) taken from %s" %(
           params.lower_bounds[0],params.lower_bounds[1],
             params.lower_bounds[2],fn))
        params.upper_bounds=list(col(origin)+col(all)-col((1,1,1)))
        print("upper bounds of (%s,%s,%s) taken from %s" %(
           params.upper_bounds[0],params.upper_bounds[1],
            params.upper_bounds[2],fn))
        params.bounds_are_absolute=True
    else:
      raise Sorry("Unable to interpret %s as map file" %(fn))

  if params.output_origin_grid_units is not None and params.keep_origin:
    params.keep_origin=False
    print("Setting keep_origin=False as output_origin_grid_units is set")
  print_statistics.make_sub_header("pdb model", out=log)
  if len(inputs.pdb_file_names)>0:
    pdb_inp = iotbx.pdb.input(file_name=inputs.pdb_file_names[0])
    pdb_hierarchy = pdb_inp.construct_hierarchy()
  if pdb_hierarchy:
    pdb_atoms = pdb_hierarchy.atoms()
    pdb_atoms.reset_i_seq()
  else:
    pdb_hierarchy=None
  # Map or map coefficients
  map_coeff = None
  input_unit_cell_grid=None
  input_unit_cell=None
  input_map_labels=None
  if (not map_data):
    # read first mtz file
    if ( (len(inputs.reflection_file_names) > 0) or
         (params.map_coefficients_file is not None) ):
      # file in phil takes precedent
      if (params.map_coefficients_file is not None):
        if (len(inputs.reflection_file_names) == 0):
          inputs.reflection_file_names.append(params.map_coefficients_file)
        else:
          inputs.reflection_file_names[0] = params.map_coefficients_file
      map_coeff = reflection_file_utils.extract_miller_array_from_file(
        file_name = inputs.reflection_file_names[0],
        label     = params.label,
        type      = "complex",
        log       = log)
      if not crystal_symmetry: crystal_symmetry=map_coeff.crystal_symmetry()
      fft_map = map_coeff.fft_map(resolution_factor=params.resolution_factor)
      fft_map.apply_sigma_scaling()
      map_data = fft_map.real_map_unpadded()
      map_or_map_coeffs_prefix=os.path.basename(
         inputs.reflection_file_names[0][:-4])
    # or read CCP4 map
    elif ( (inputs.ccp4_map is not None) or
           (params.ccp4_map_file is not None) ):
      if (params.ccp4_map_file is not None):
        af = any_file(params.ccp4_map_file)
        if (af.file_type == 'ccp4_map'):
          inputs.ccp4_map = af.file_content
          inputs.ccp4_map_file_name = params.ccp4_map_file
      print_statistics.make_sub_header("CCP4 map", out=log)
      ccp4_map = inputs.ccp4_map
      ccp4_map.show_summary(prefix="  ",out=log)
      if not crystal_symmetry: crystal_symmetry=ccp4_map.crystal_symmetry()
      map_data = ccp4_map.data #map_data()
      input_unit_cell_grid=ccp4_map.unit_cell_grid
      input_unit_cell=ccp4_map.unit_cell_parameters
      input_map_labels=ccp4_map.get_labels()

      if inputs.ccp4_map_file_name.endswith(".ccp4"):
        map_or_map_coeffs_prefix=os.path.basename(
          inputs.ccp4_map_file_name[:-5])
      else:
        map_or_map_coeffs_prefix=os.path.basename(
          inputs.ccp4_map_file_name[:-4])
  else: # have map_data
    map_or_map_coeffs_prefix=None

  if params.half_map_list and (not half_map_data_list):
    if not params.extract_unique:
      raise Sorry("Can only use half_map_with extract_unique")
    print ("Reading half-maps",params.half_map_list)
    half_map_data_list=[]
    half_map_labels_list=[]
    for fn in params.half_map_list:
      print("Reading half map from %s" %(fn),file=log)
      af = any_file(fn)
      print_statistics.make_sub_header("CCP4 map", out=log)
      h_ccp4_map = af.file_content
      h_ccp4_map.show_summary(prefix="  ",out=log)
      h_map_data = h_ccp4_map.data
      half_map_data_list.append(h_map_data)
      half_map_labels_list.append(h_ccp4_map.get_labels())

  if params.map_scale_factor:
    print("Applying scale factor of %s to map data on read-in" %(
       params.map_scale_factor))
    map_data=map_data*params.map_scale_factor

  if params.output_origin_grid_units is not None:
    origin_to_match=tuple(params.output_origin_grid_units)
  else:
    origin_to_match=None

  if origin_to_match:
    sc=[]
    for x,o,a in zip(crystal_symmetry.unit_cell().parameters()[:3],
        origin_to_match,
        map_data.all()):
      sc.append(-x*o/a)
    shift_cart_for_origin_to_match=tuple(sc)
  else:
    origin_to_match=None
    shift_cart_for_origin_to_match=None

  if crystal_symmetry and not inputs.crystal_symmetry:
    inputs.crystal_symmetry=crystal_symmetry

  # final check that map_data exists
  if(map_data is None):
    raise Sorry("Map or map coefficients file is needed.")

  if len(inputs.pdb_file_names)>0:
    output_prefix=os.path.basename(inputs.pdb_file_names[0])[:-4]
  else:
    output_prefix=map_or_map_coeffs_prefix

  if not pdb_hierarchy: # get an empty hierarchy
    from cctbx.array_family import flex
    pdb_hierarchy=iotbx.pdb.input(
      source_info='',lines=flex.split_lines('')).construct_hierarchy()
  xray_structure = pdb_hierarchy.extract_xray_structure(
    crystal_symmetry=inputs.crystal_symmetry)
  xray_structure.show_summary(f=log)
  #
  if not params.selection: params.selection="all"
  selection = pdb_hierarchy.atom_selection_cache().selection(
    string = params.selection)
  if selection.size():
    print_statistics.make_sub_header("atom selection", out=log)
    print("Selection string: selection='%s'"%params.selection, file=log)
    print("  selects %d atoms from total %d atoms."%(selection.count(True),
        selection.size()), file=log)
  sites_cart_all = xray_structure.sites_cart()
  sites_cart = sites_cart_all.select(selection)
  selection = xray_structure.selection_within(
    radius    = params.selection_radius,
    selection = selection)

  if not ncs_object:
    from mmtbx.ncs.ncs import ncs
    ncs_object=ncs()
    if params.symmetry_file:
      ncs_object.read_ncs(params.symmetry_file,log=log)
      print("Total of %s operators read" %(ncs_object.max_operators()), file=log)
  if not ncs_object or ncs_object.max_operators()<1:
      print("No symmetry available", file=log)
  if ncs_object:
    n_ops=max(1,ncs_object.max_operators())
  else:
    n_ops=1

  # Get sequence if extract_unique is set
  sequence=None
  if params.extract_unique or params.mask_select:
    if params.sequence_file:
      if n_ops > 1: # get unique part of sequence
        remove_duplicates=True
      else:
        remove_duplicates=False
      from iotbx.bioinformatics import get_sequences
      sequence=(" ".join(get_sequences(file_name=params.sequence_file,
        remove_duplicates=remove_duplicates)))

    if params.chain_type in ['None',None]: params.chain_type=None
    if sequence and not params.molecular_mass:
      # get molecular mass from sequence
      from iotbx.bioinformatics import text_from_chains_matching_chain_type
      if params.chain_type in [None,'PROTEIN']:
        n_protein=len(text_from_chains_matching_chain_type(
          text=sequence,chain_type='PROTEIN'))
      else:
        n_protein=0
      if params.chain_type in [None,'RNA']:
        n_rna=len(text_from_chains_matching_chain_type(
          text=sequence,chain_type='RNA'))
      else:
        n_rna=0
      if params.chain_type in [None,'DNA']:
        n_dna=len(text_from_chains_matching_chain_type(
         text=sequence,chain_type='DNA'))
      else:
        n_dna=0
      params.molecular_mass=n_ops*(n_protein*110+(n_rna+n_dna)*330)
      print("\nEstimate of molecular mass is %.0f " %(params.molecular_mass), file=log)
  if params.density_select or params.mask_select:
    print_statistics.make_sub_header(
    "Extracting box around selected density and writing output files", out=log)
  else:
   print_statistics.make_sub_header(
    "Extracting box around selected atoms and writing output files", out=log)
  #
  if params.value_outside_atoms=='mean':
    print("\nValue outside atoms mask will be set to mean inside mask", file=log)
  if params.get_half_height_width and params.density_select:
    print("\nHalf width at half height will be used to id boundaries", file=log)

  if params.soft_mask and sites_cart_all.size()>0:
    print("\nSoft mask will be applied to model-based mask", file=log)
  elif params.soft_mask:
    print ("\nSoft mask will be applied to outside of map box",file=log)
  if params.keep_map_size:
    print("\nEntire map will be kept (not cutting out region)", file=log)
  if params.restrict_map_size:
    print("\nOutput map will be within input map", file=log)
  if params.lower_bounds and params.upper_bounds:
    print("Bounds for cut out map are (%s,%s,%s) to (%s,%s,%s)" %(
     tuple(list(params.lower_bounds)+list(params.upper_bounds))), file=log)

  if mask_data:
    mask_data=mask_data.as_double()
  box = mmtbx.utils.extract_box_around_model_and_map(
    xray_structure   = xray_structure,
    map_data         = map_data.as_double(),
    mask_data        = mask_data,
    box_cushion      = params.box_cushion,
    selection        = selection,
    mask_select      = params.mask_select,
    density_select   = params.density_select,
    threshold        = params.density_select_threshold,
    get_half_height_width = params.get_half_height_width,
    mask_atoms       = params.mask_atoms,
    soft_mask        = params.soft_mask,
    soft_mask_radius = params.soft_mask_radius,
    mask_atoms_atom_radius = params.mask_atoms_atom_radius,
    value_outside_atoms = params.value_outside_atoms,
    keep_map_size         = params.keep_map_size,
    restrict_map_size     = params.restrict_map_size,
    lower_bounds          = params.lower_bounds,
    upper_bounds          = params.upper_bounds,
    bounds_are_absolute   = params.bounds_are_absolute,
    zero_outside_original_map   = params.zero_outside_original_map,
    extract_unique        = params.extract_unique,
    target_ncs_au_file    = params.target_ncs_au_file,
    regions_to_keep       = params.regions_to_keep,
    box_buffer            = params.box_buffer,
    soft_mask_extract_unique = params.soft_mask_extract_unique,
    mask_expand_ratio = params.mask_expand_ratio,
    keep_low_density      = params.keep_low_density,
    chain_type            = params.chain_type,
    sequence              = sequence,
    solvent_content       = params.solvent_content,
    molecular_mass        = params.molecular_mass,
    resolution            = params.resolution,
    ncs_object            = ncs_object,
    symmetry              = params.symmetry,
    half_map_data_list    = half_map_data_list,
    )

  ph_box = pdb_hierarchy.select(selection)
  ph_box.adopt_xray_structure(box.xray_structure_box)
  box.hierarchy=ph_box

  if params.mask_select:
    print("\nSolvent content used in mask_select: %.3f " %(
      box.get_solvent_content()),file=log)
  if (inputs and
    inputs.crystal_symmetry and inputs.ccp4_map and
    inputs.crystal_symmetry.unit_cell().parameters() and
     inputs.ccp4_map.unit_cell_parameters  ) and (
       inputs.crystal_symmetry.unit_cell().parameters() !=
       inputs.ccp4_map.unit_cell_parameters):
    print("\nNOTE: Input CCP4 map is only part of unit cell:", file=log)
    print("Full unit cell ('unit cell parameters'): "+\
      "(%.1f, %.1f, %.1f, %.1f, %.1f, %.1f) A" %tuple(
        inputs.ccp4_map.unit_cell_parameters), file=log)
    print("Size of CCP4 map 'map unit cell':        "+\
      "(%.1f, %.1f, %.1f, %.1f, %.1f, %.1f) A" %tuple(
       inputs.crystal_symmetry.unit_cell().parameters()), file=log)
    print("Full unit cell as grid units: (%s, %s, %s)" %(
      inputs.ccp4_map.unit_cell_grid), file=log)
    print("Map unit cell as grid units:  (%s, %s, %s)" %(
      map_data.all()), file=log)

    box.unit_cell_parameters_from_ccp4_map=inputs.ccp4_map.unit_cell_parameters
    box.unit_cell_parameters_deduced_from_map_grid=\
       inputs.crystal_symmetry.unit_cell().parameters()

  else:
    box.unit_cell_parameters_from_ccp4_map=None
    box.unit_cell_parameters_deduced_from_map_grid=None

  if box.pdb_outside_box_msg:
    print(box.pdb_outside_box_msg, file=log)

  # NOTE: box object is always shifted to place origin at (0,0,0)

  # NOTE ON ORIGIN SHIFTS:  The shifts are applied locally here. The box
  #  object is not affected and always has the origin at (0,0,0)
  #  output_box is copy of box with shift_cart corresponding to the output
  #   files. Normally this is the same as the original shift_cart. However
  #   if user has specified a new output origin it will differ.

  # For output files ONLY:
  #  keep_origin==False leave origin at (0,0,0)
  #  keep_origin==True: we shift everything back to where it was,
  #  output_origin_grid_units=10,10,10: output origin is at (10,10,10)

  # ncs_object is original
  #  box.ncs_object is shifted by shift_cart
  #  output_box.ncs_object is shifted back by -new shift_cart

  # Additional note on output unit_cell and grid_units.
  # The ccp4-style output map can specify the unit cell and grid units
  #  corresponding to that cell.  This can be separate from the origin and
  #  number of grid points in the map as written.  If specified, write these
  #  out to the output ccp4 map and also use this unit cell for writing
  #  any output PDB files

  from copy import deepcopy
  output_box=deepcopy(box)  # won't use box below here except to return it

  print("\nBox cell dimensions: (%.2f, %.2f, %.2f) A" %(
      box.box_crystal_symmetry.unit_cell().parameters()[:3]), file=log)

  if box.shift_cart:
     print("Working origin moved from grid position of"+\
        ": (%d, %d, %d) to (0,0,0) " %(
        tuple(box.origin_shift_grid_units(reverse=True))), file=log)
     print("Working origin moved from  coordinates of:"+\
        " (%.2f, %.2f, %.2f) A to (0,0,0)\n" %(
         tuple(-col(box.shift_cart))), file=log)

  if (params.keep_origin):
    print("\nRestoring original position for output files", file=log)
    print("Origin will be at grid position of"+\
        ": (%d, %d, %d) " %(
        tuple(box.origin_shift_grid_units(reverse=True))), file=log)
    print("\nOutput files will be in same location as original", end=' ', file=log)
    if not params.keep_map_size:
      print("just cut out.", file=log)
    else:
      print("keeping entire map", file=log)
    print("Note that output maps are only valid in the cut out region.\n", file=log)

  else:
    if origin_to_match:
      output_box.shift_cart=shift_cart_for_origin_to_match
      if params.output_origin_grid_units:
        print("Output map origin to be shifted to match target", file=log)
      print("Placing origin at grid point (%s, %s, %s)" %(
        origin_to_match)+"\n"+ \
        "Final coordinate shift for output files: (%.2f,%.2f,%.2f) A\n" %(
        tuple(col(output_box.shift_cart)-col(box.shift_cart))), file=log)
    elif box.shift_cart:
      output_box.shift_cart=(0,0,0) # not shifting back
      print("Final origin will be at (0,0,0)", file=log)
      print("Final coordinate shift for output files: (%.2f,%.2f,%.2f) A\n" %(
        tuple(col(output_box.shift_cart)-col(box.shift_cart))), file=log)
    else:
      print("\nOutput files are in same location as original and origin "+\
        "is at (0,0,0)\n", file=log)

  print("\nBox grid: (%s, %s, %s) " %(output_box.map_box.all()),file=log)
  ph_output_box_output_location = ph_box.deep_copy()
  if output_box.shift_cart:  # shift coordinates and NCS back by shift_cart
    # NOTE output_box.shift_cart could be different than box.shift_cart if
    #  there is a target position for the origin and it is not the same as the
    #  original origin.
    sites_cart = output_box.shift_sites_cart_back(
      output_box.xray_structure_box.sites_cart())
    xrs_offset = ph_output_box_output_location.extract_xray_structure(
        crystal_symmetry=output_box.xray_structure_box.crystal_symmetry()
          ).replace_sites_cart(new_sites = sites_cart)
    ph_output_box_output_location.adopt_xray_structure(xrs_offset)

    if output_box.ncs_object:
      output_box.ncs_object=output_box.ncs_object.coordinate_offset(
         tuple(-col(output_box.shift_cart)))
    shift_back=True
  else:
    shift_back=False

  if params.keep_input_unit_cell_and_grid and \
       (input_unit_cell_grid is not None) and \
       (input_unit_cell is not None):
    params.output_unit_cell=input_unit_cell
    params.output_unit_cell_grid=input_unit_cell_grid
    print("Setting output unit cell parameters and unit cell grid to"+\
      " match\ninput map file", file=log)

  if params.output_unit_cell: # Set output unit cell parameters
    from cctbx import crystal
    output_crystal_symmetry=crystal.symmetry(
      unit_cell=params.output_unit_cell, space_group="P1")
    output_unit_cell=output_crystal_symmetry.unit_cell()
    print("Output unit cell set to: %.2f, %.2f, %.2f, %.2f, %.2f, %.2f)" %tuple(
        output_crystal_symmetry.unit_cell().parameters()), file=log)
  else:
    output_crystal_symmetry=None

  # =============  Check/set output unit cell grid and cell parameters =======
  if params.output_unit_cell_grid or output_crystal_symmetry:
    if params.output_unit_cell_grid:
      output_unit_cell_grid=params.output_unit_cell_grid
    else:
      output_unit_cell_grid=output_box.map_box.all()
    print("Output unit cell grid set to: (%s, %s, %s)" %tuple(
        output_unit_cell_grid), file=log)

    expected_output_abc=[]
    box_spacing=[]
    output_spacing=[]
    box_abc=output_box.xray_structure_box.\
         crystal_symmetry().unit_cell().parameters()[:3]
    if output_crystal_symmetry:
      output_abc=output_crystal_symmetry.unit_cell().parameters()[:3]
    else:
      output_abc=[None,None,None]
    for a_box,a_output,n_box,n_output in zip(
        box_abc,
        output_abc,
        output_box.map_box.all(),
        output_unit_cell_grid):
      expected_output_abc.append(a_box*n_output/n_box)
      box_spacing.append(a_box/n_box)
      if output_crystal_symmetry:
        output_spacing.append(a_output/n_output)
      else:
        output_spacing.append(a_box/n_box)

    if output_crystal_symmetry: # make sure it is compatible...
      r0=expected_output_abc[0]/output_abc[0]
      r1=expected_output_abc[1]/output_abc[1]
      r2=expected_output_abc[2]/output_abc[2]
      from libtbx.test_utils import approx_equal
      if not approx_equal(r0,r1,eps=0.001) or not approx_equal(r0,r2,eps=0.001):
        print("WARNING: output_unit_cell and cell_grid will "+\
          "change ratio of grid spacing.\nOld spacings: "+\
         "(%.2f, %.2f, %.2f) A " %(tuple(box_spacing))+\
        "\nNew spacings:  (%.2f, %.2f, %.2f) A \n" %(tuple(output_spacing)), file=log)
    else:
      output_abc=expected_output_abc

    from cctbx import crystal
    output_crystal_symmetry=crystal.symmetry(
          unit_cell=list(output_abc)+[90,90,90], space_group="P1")
    print("Output unit cell will be:  (%.2f, %.2f, %.2f, %.2f, %.2f, %.2f)\n"%(
       tuple(output_crystal_symmetry.unit_cell().parameters())), file=log)

  else:
    output_unit_cell_grid = output_box.map_box.all()
    output_crystal_symmetry=output_box.xray_structure_box.crystal_symmetry()
  # ==========  Done check/set output unit cell grid and cell parameters =====

  if write_output_files:
    # Write PDB file
    if ph_box.overall_counts().n_residues>0:

      if(params.output_file_name_prefix is None):
        file_name = "%s_box.pdb"%output_prefix
      else: file_name = "%s.pdb"%params.output_file_name_prefix
      ph_output_box_output_location.write_pdb_file(file_name=file_name,
          crystal_symmetry = output_crystal_symmetry)
      print("Writing boxed PDB with box unit cell to %s" %(
          file_name), file=log)

    # Write NCS file if NCS
    if output_box.ncs_object and output_box.ncs_object.max_operators()>0:
      if(params.output_file_name_prefix is None):
        output_symmetry_file = "%s_box.ncs_spec"%output_prefix
      else:
        output_symmetry_file = "%s.ncs_spec"%params.output_file_name_prefix
      output_box.ncs_object.format_all_for_group_specification(
          file_name=output_symmetry_file)

      print("\nWriting symmetry to %s" %( output_symmetry_file), file=log)

    # Write ccp4 map.
    if("ccp4" in params.output_format):
     if(params.output_file_name_prefix is None):
       file_name = "%s_box.ccp4"%output_prefix
     else: file_name = "%s.ccp4"%params.output_file_name_prefix
     from iotbx.mrcfile import create_output_labels
     if params.extract_unique:
       program_name='map_box using extract_unique'
       limitations=["extract_unique"]
     else:
       program_name='map_box'
       limitations=[]
     labels=create_output_labels(program_name=program_name,
       input_file_name=inputs.ccp4_map_file_name,
       input_labels=input_map_labels,
       limitations=limitations,
       output_labels=params.output_map_labels)
     output_box.write_ccp4_map(file_name=file_name,
       output_crystal_symmetry=output_crystal_symmetry,
       output_mean=params.output_ccp4_map_mean,
       output_sd=params.output_ccp4_map_sd,
       output_unit_cell_grid=output_unit_cell_grid,
       shift_back=shift_back,
       output_map_labels=labels,
       output_external_origin=params.output_external_origin)
     print("Writing boxed map "+\
          "to CCP4 formatted file:   %s"%file_name, file=log)
     if not params.half_map_list:
        params.half_map_list=[]
     if not output_box.map_box_half_map_list:
       output_box.map_box_half_map_list=[]
     if not half_map_labels_list:
       half_map_labels_list=len(output_box.map_box_half_map_list)*[None]
     for hm,labels,fn in zip(
       output_box.map_box_half_map_list,
       half_map_labels_list,
       params.half_map_list):  # half maps matching
       labels=create_output_labels(program_name=program_name,
         input_file_name=fn,
         input_labels=labels,
         limitations=limitations,
         output_labels=params.output_map_labels)
       hm_fn="%s_box.ccp4" %( ".".join(os.path.basename(fn).split(".")[:-1]))
       output_box.write_ccp4_map(file_name=hm_fn,
         map_data=hm,
         output_crystal_symmetry=output_crystal_symmetry,
         output_mean=params.output_ccp4_map_mean,
         output_sd=params.output_ccp4_map_sd,
         output_unit_cell_grid=output_unit_cell_grid,
         shift_back=shift_back,
         output_map_labels=labels,
         output_external_origin=params.output_external_origin)
       print ("Writing boxed half map to: %s " %(hm_fn),file=log)

    # Write xplor map.  Shift back if keep_origin=True
    if("xplor" in params.output_format):
     if(params.output_file_name_prefix is None):
       file_name = "%s_box.xplor"%output_prefix
     else: file_name = "%s.xplor"%params.output_file_name_prefix
     output_box.write_xplor_map(file_name=file_name,
         output_crystal_symmetry=output_crystal_symmetry,
         output_unit_cell_grid=output_unit_cell_grid,
         shift_back=shift_back,)
     print("Writing boxed map "+\
         "to X-plor formatted file: %s"%file_name, file=log)

    # Write mtz map coeffs.  Shift back if keep_origin=True
    if("mtz" in params.output_format):
     if(params.output_file_name_prefix is None):
       file_name = "%s_box.mtz"%output_prefix
     else: file_name = "%s.mtz"%params.output_file_name_prefix

     print("Writing map coefficients "+\
         "to MTZ file: %s"%file_name, file=log)
     if(map_coeff is not None):
       d_min = map_coeff.d_min()
     elif params.resolution is not None:
       d_min = params.resolution
     else:
       d_min = maptbx.d_min_from_map(map_data=output_box.map_box,
         unit_cell=output_box.xray_structure_box.unit_cell())
     output_box.map_coefficients(d_min=d_min,
       scale_max=params.scale_max,
       resolution_factor=params.resolution_factor, file_name=file_name,
       shift_back=shift_back)

  print(file=log)
  return box

# =============================================================================
# GUI-specific class for running command
from libtbx import runtime_utils
from wxGUI2 import utils

def validate_params(params):
  return True

class launcher(runtime_utils.target_with_save_result):
  def run(self):
    utils.safe_makedirs(self.output_dir)
    os.chdir(self.output_dir)
    result = run(args=self.args, log=sys.stdout)
    return result

# =============================================================================

if (__name__ == "__main__"):
  run(args=sys.argv[1:])