#!/usr/bin/python -u
## @package localsettings
#
#Gregory Burte - 3 MArch 2019
#Update of logging system
#
#Requires Logging
#
# Previous comments
#---------------------
# Dbus-service for local settings.
#
# Below code needs a major check and cleanup. A not complete list would be:
# - get rid of the tracing, just use the standard logging modul, as also done in dbus_conversions for example
# - use argparse.ArgumentParser, so get rid of usage()
# - probably remove a lot of dbus code and replace by items from vedbus.py from velib_python

# The local-settings-dbus-service provides the local-settings storage in non-volatile-memory.
# The settings are stored in the settings.xml file. At startup the xml file is parsed and
# the dbus-service with his paths are created from the content of the xml file.
# <Settings></Settings> is the root of the xml file. A group of settings is a child of
# the root <Settings>. A setting is a child of a group and contains text.
# (e.g. <LogInterval>900</LogInterval>.
# Example 1:
# <Settings>
# 	<Logging>
#		<LogInterval>900</LogInterval>
#	</Logging>
# </Settings>
# This will be parsed as an dbus-object-path /Settings/Logging/LogInterval.
#
# These are set as an attribute of a (setting) element.
# Example 2: <Brigthness type="i" min="0" max="100" default="100">100</Brigthness>
# Example 3: <LogInterval type="i" min="5" default="900">900</LogInterval>
# Example 4: <LogPath type="s" default=".">.</LogPath>
# Settings or a group of settings can be set to default. A setting (and group) can be
# added by means of dbus. And of course a setting can be changed by means of dbus.
# By means of the file settingchanges.xml settings can be added or deleted. This file
# is processed at startup and then deleted.
#

##

# Python imports
from dbus.mainloop.glib import DBusGMainLoop
import dbus
import dbus.service
from gobject import timeout_add, source_remove, MainLoop
from os import path, getpid, remove, rename, _exit, environ
import sys
import signal
from lxml import etree
import getopt
import errno
import platform
import os
import re

# Victron imports
sys.path.insert(1, os.path.join(os.path.dirname(__file__), './ext/velib_python'))
#import tracing
import logging

## Major version.
FIRMWARE_VERSION_MAJOR = 0x01
## Minor version.
FIRMWARE_VERSION_MINOR = 0x0c
## Localsettings version.
version = (FIRMWARE_VERSION_MAJOR << 8) | FIRMWARE_VERSION_MINOR

## Traces (info / debug) setup
pathTraces = '/log/'
traceFileName = 'localsettingstraces'
tracingEnabled = False
traceToConsole = False
traceToFile = False
traceDebugOn = False

## The dbus bus and bus-name.
bus = None
busName = None

## Dbus service name and interface name(s).
dbusName = 'com.victronenergy.settings'
InterfaceBusItem = 'com.victronenergy.BusItem'
InterfaceSettings = 'com.victronenergy.Settings'

## The dictonary's containing the settings and the groups.
settings = {}
groups = []

## Index values for settings.
VALUE = 0
ATTRIB = 1

## ATTRIB keywords.
TYPE='type'
MIN='min'
MAX='max'
DEFAULT='default'
SILENT='silent'

## Supported types for convert xml-text to value.
supportedTypes = {
		'i':int,
		's':str,
		'f':float,
}

## The list of MyDbusService(s).
myDbusServices = []
myDbusGroupServices = []

## The main group dbus-service.
myDbusMainGroupService = None

## Save settings timeout.
timeoutSaveSettingsEventId = None
timeoutSaveSettingsTime = 2 # Timeout value in seconds.

## File names.
fileSettings = 'settings.xml'
fileSettingChanges = 'settingchanges.xml'
newFileExtension = '.new'
newFileSettings = fileSettings + newFileExtension

## Path(s) definitions.
pathSettings = '/data/conf/'
sysSettingsDir = '/etc/venus/settings.d'

## Settings file version tag, encoding and root-element.
settingsTag = 'version'
settingsVersion = '1.0'
settingsEncoding = 'UTF-8'
settingsRootName = 'Settings'

## Indicates if settings are added
settingsAdded = False

class MyDbusObject(dbus.service.Object):
	global InterfaceBusItem

	## Constructor of MyDbusObject
	#
	# Creates the dbus-object under the given bus-name (dbus-service-name).
	# @param busName Return value from dbus.service.BusName, see run()).
	# @param objectPath The dbus-object-path (e.g. '/Settings/Logging/LogInterval').
	def __init__(self, busName, objectPath):
		dbus.service.Object.__init__(self, busName, objectPath)

	## Dbus method GetValue
	# Returns the value of the dbus-object-path (the settings).
	# When the object-path is a group a -1 is returned.
	# @return setting A setting value or -1 (error)
	@dbus.service.method(InterfaceBusItem, out_signature = 'v')
	def GetValue(self):
		global settings
		global groups

		logger.debug('GetValue %s' % self._object_path)
		if self._object_path in groups:
			prefix = self._object_path + '/'
			return dbus.Dictionary({ k[len(prefix):]: v[VALUE] \
				for k, v in settings.iteritems() \
				if k.startswith(prefix) and len(k)>len(prefix) },
				signature=dbus.Signature('sv'), variant_level=1)
		return settings[self._object_path][VALUE]

	## Dbus method GetText
	# Returns the value as string of the dbus-object-path (the settings).
	# When the object-path is a group a '' is returned.
	# @return setting A setting value or '' (error)
	@dbus.service.method(InterfaceBusItem, out_signature = 's')
	def GetText(self):
		global settings

		if self._object_path in groups:
			return ''
		return str(settings[self._object_path][VALUE])

	## Dbus method SetValue
	# Sets the value of a setting. When the type of the setting is a integer or float,
	# the new value is checked according to minimum and maximum.
	# @param value The new value for the setting.
	# @return completion-code When successful a 0 is return, and when not a -1 is returned.
	@dbus.service.method(InterfaceBusItem, in_signature = 'v', out_signature = 'i')
	def SetValue(self, value):
		global settings
		global supportedTypes

		logger.debug('SetValue %s' % self._object_path)
		if self._object_path in groups:
			return -1
		okToSave = True
		v = value
		path = self._object_path
		if TYPE in settings[path][ATTRIB]:
			itemType = settings[path][ATTRIB][TYPE]
			if itemType in supportedTypes:
				try:
					v = convertToType(itemType, value)
					if MIN in settings[path][ATTRIB]:
						if v < convertToType(itemType, settings[path][ATTRIB][MIN]):
							okToSave = False
					if MAX in settings[path][ATTRIB]:
						if v > convertToType(itemType, settings[path][ATTRIB][MAX]):
							okToSave = False
				except:
					okToSave = False
		if okToSave == True:
			if v != settings[path][VALUE]:
				self._setValue(v)
			return 0
		else:
			return -1

	@dbus.service.method(InterfaceBusItem, out_signature = 'v')
	def GetMin(self):
		if MIN in settings[self._object_path][ATTRIB]:
			return settings[self._object_path][ATTRIB][MIN]
		else:
			return 0

	@dbus.service.method(InterfaceBusItem, out_signature = 'v')
	def GetMax(self):
		if MAX in settings[self._object_path][ATTRIB]:
			return settings[self._object_path][ATTRIB][MAX]
		else:
			return 0

	## Sets the value and starts the time-out for saving to the settings-xml-file.
	# @param value The new value for the setting.
	def _setValue(self, value, printLog=True, sendAttributes=False):
		global settings

		if printLog and settings[self._object_path][ATTRIB].get(SILENT) != 'True':
			logger.info('Setting %s changed. Old: %s, New: %s' % (self._object_path, settings[self._object_path][VALUE], value))

		settings[self._object_path][VALUE] = value
		self._startTimeoutSaveSettings()
		text = self.GetText()
		change = {'Value':value, 'Text':text}
		if sendAttributes:
			change.update({'Min': self.GetMin(), 'Max': self.GetMax(), 'Default': self.GetDefault()})
		self.PropertiesChanged(change)

	@dbus.service.signal(InterfaceBusItem, signature = 'a{sv}')
	def PropertiesChanged(self, changes):
		logger.debug('signal PropertiesChanged')

	@dbus.service.signal(InterfaceSettings, signature = '')
	def ObjectPathsChanged(self):
		logger.debug('signal ObjectPathsChanged')

	## Method for starting the time-out for saving to the settings-xml-file.
	# (Re)Starts the time-out. When after x time no settings are changed,
	# the settings-xml-file is saved.
	def _startTimeoutSaveSettings(self):
		global timeoutSaveSettingsEventId
		global timeoutSaveSettingsTime

		if timeoutSaveSettingsEventId is not None:
			source_remove(timeoutSaveSettingsEventId)
			timeoutSaveSettingsEventId = None
		timeoutSaveSettingsEventId = timeout_add(timeoutSaveSettingsTime*1000, saveSettingsCallback)

	## Dbus method GetDefault.
	# Returns the default value of a setting.
	# @return value The default value or -1 (error or no default set)
	@dbus.service.method(InterfaceBusItem, out_signature = 'v')
	def GetDefault(self):
		global settings

		logger.info('GetDefault %s' % self._object_path)
		path = self._object_path
		if path in groups:
			return -1
		try:
			type = settings[path][ATTRIB][TYPE]
			value = convertToType(type, settings[path][ATTRIB][DEFAULT])
			return value
		except:
			logger.error('Could not get default for %s %s' % (path, settings[path][ATTRIB].items()))
			return -1

	## Dbus method SetDefault.
	# Sets the value of the setting to default. When the object-path is a group,
	# it sets the default for all the settings in that group.
	# @return completion-code When successful a 0 is return, and when not a -1 is returned.
	@dbus.service.method(InterfaceBusItem, out_signature = 'i')
	def SetDefault(self):
		global myDbusServices
		global settings

		logger.info('SetDefault %s' % self._object_path)
		try:
			path = self._object_path
			if path in groups:
				for service in myDbusServices:
					servicePath = service._object_path
					if path in servicePath:
						service.SetValue(settings[servicePath][ATTRIB][DEFAULT])
			else:
				self.SetValue(settings[path][ATTRIB][DEFAULT])
			return 0
		except:
			logger.error('Could not set default for %s %s' % (path, settings[path][ATTRIB].items()))
			return -1

	## Dbus method GetSilent.
	# @return 1 if the setting is silent, 0 is it is not. Returns -1 if an error occurred.
	@dbus.service.method(InterfaceBusItem, out_signature = 'i')
	def GetSilent(self):
		global settings

		logger.debug('GetSilent %s' % self._object_path)
		path = self._object_path
		if path in groups:
			return -1
		try:
			value = settings[path][ATTRIB].get(SILENT) == 'True'
			return 1 if value else 0
		except:
			logger.error('Could not get silent property for %s %s' % (path, settings[path][ATTRIB].items()))
			return -1

	## Dbus method AddSetting.
	# Add a new setting by the given parameters. The object-path must be a group.
	# Example 1: dbus /Settings AddSetting Groupname Settingname 100 i 0 100
	# Example 2: dbus /Settings AddSetting Groupname Settingname '/home/root' s 0 0
	# When the new setting is of type string the minimum and maximum will be ignored.
	# @param group The group-name.
	# @param name The setting-name.
	# @param defaultValue The default value (and initial value) of the setting.
	# @param itemType Types 's' string, 'i' integer or 'f' float.
	# @param minimum The minimum value.
	# @param maximum The maximum value.
	# @return completion-code When successful a 0 is return, and when not a -1 is returned.
	@dbus.service.method(InterfaceSettings, in_signature = 'ssvsvv', out_signature = 'i')
	def AddSetting(self, group, name, defaultValue, itemType, minimum, maximum):
		return self.addSetting(group, name, defaultValue, itemType, minimum, maximum, silent=False)

	@dbus.service.method(InterfaceSettings, in_signature = 'ssvsvv', out_signature = 'i')
	def AddSilentSetting(self, group, name, defaultValue, itemType, minimum, maximum):
		return self.addSetting(group, name, defaultValue, itemType, minimum, maximum, silent=True)

	def addSetting(self, group, name, defaultValue, itemType, minimum, maximum, silent):
		global groups
		global settings
		global defaults
		global busName
		global myDbusGroupServices
		global myDbusServices
		global settingsAdded

		logger.debug('AddSetting %s %s %s' % (self._object_path, group, name))
		if self._object_path not in groups:
			return -1

		if group.startswith('/') or group == '':
			groupPath = self._object_path + str(group)
		else:
			groupPath = self._object_path + '/' + str(group)

		if name.startswith('/'):
			itemPath = groupPath + str(name)
		else:
			itemPath = groupPath + '/' + str(name)

		# A prefixing underscore is an escape char: don't allow it in a normal path
		if "/_" in itemPath:
			return -2

		if itemType not in supportedTypes:
			return -3

		try:
			value = convertToType(itemType, defaultValue)
			if type(value) != str:
				min = convertToType(itemType, minimum)
				max = convertToType(itemType, maximum)
				if min is 0 and max is 0:
					attributes = {TYPE:str(itemType), DEFAULT:str(value)}
				elif value >= min and value <= max:
					attributes = {TYPE:str(itemType), DEFAULT:str(value), MIN:str(min), MAX:str(max)}
			else:
				attributes = {TYPE:str(itemType), DEFAULT:str(value)}
			attributes[SILENT] = str(silent)
		except:
			return -4

		if not groupPath in groups:
			groups.append(groupPath)
			groupObject = MyDbusObject(busName, groupPath)
			myDbusGroupServices.append(groupObject)

		if not itemPath in settings:
			# New setting
			myDbusObject = MyDbusObject(busName, itemPath)
			myDbusServices.append(myDbusObject)
		else:
			# Existing setting
			if settings[itemPath][ATTRIB][TYPE] != attributes[TYPE]:
				return -5

			for service in myDbusServices:
				if service._object_path == itemPath:
					myDbusObject = service
					break

			unmatched = set(settings[itemPath][ATTRIB].items()) ^ set(attributes.items())
			if len(unmatched) == 0:
				# There are no changes
				return 0

			# There are changes, save them while keeping the current value.
			value = settings[itemPath][VALUE]

		settings[itemPath] = [0, {}]
		settings[itemPath][ATTRIB] = attributes
		logger.info('Added new setting %s. default:%s, type:%s, min:%s, max: %s, silent: %s' % (itemPath, defaultValue, itemType, minimum, maximum, silent))
		myDbusObject._setValue(value, printLog=False, sendAttributes=True)
		settingsAdded = True
		self._startTimeoutSaveSettings()
		return 0

# This object will send an overview of all available D-Bus paths and their values in the settings service if
# a GetValue or GetText is issued on the root of the service.
class RootObject(dbus.service.Object):
	def __init__(self, busname):
		dbus.service.Object.__init__(self, busname, '/')

	@dbus.service.method(InterfaceBusItem, out_signature='v')
	def GetValue(self):
		values = dict((k[1:], v[VALUE]) for k, v in settings.iteritems())
		return dbus.Dictionary(values, signature=dbus.Signature('sv'), variant_level=1)

	@dbus.service.method(InterfaceBusItem, out_signature='v')
	def GetText(self):
		values = dict((k[1:], str(v[VALUE])) for k, v in settings.iteritems())
		return values

## The callback method for saving the settings-xml-file.
# Calls the parseDictonaryToXmlFile with the dictonary settings and settings-xml-filename.
def saveSettingsCallback():
	global timeoutSaveSettingsEventId
	global settings
	global fileSettings
	global myDbusMainGroupService
	global settingsAdded

	logger.debug('Saving settings to file')
	source_remove(timeoutSaveSettingsEventId)
	timeoutSaveSettingsEventId = None
	parseDictonaryToXmlFile(settings, fileSettings)
	if settingsAdded is True:
		settingsAdded = False
		myDbusMainGroupService.ObjectPathsChanged()

## Method for converting a value the the given type.
# When the type is not supported it simply returns the value as is.
# @param type The type to convert to.
# @param value The value to convert.
# @return value The converted value (if type is supported).
def convertToType(type, value):
	if type in supportedTypes:
		return supportedTypes[type](value)
	else:
		return value

## Method for parsing the file to the given dictonary.
# The dictonary will be in following format {dbus-object-path, [value, {attributes}]}.
# When a array is given for the groups, the found groups are appended.
# @param file The filename (path can be included, e.g. /home/root/localsettings/settings.xml).
# @param dictonaryItems The dictonary for the settings.
# @param arrayGroups The array for the groups.
# @param filter A filter used for filtering in settingchanges.xml for example "Add".
def parseXmlFileToDictonary(file, dictonaryItems, arrayGroups, filter):

	parser = etree.XMLParser(remove_blank_text=True)
	tree = etree.parse(file, parser)
	root = tree.getroot()
	logger.debug('parseXmlFileToDictonary %s:' % file)
	docinfo = tree.docinfo
	logger.debug("docinfo version %s" % docinfo.xml_version)
	logger.debug("docinfo encoding %s" % docinfo.encoding)
	logger.debug("settings version %s" % root.attrib)
	logger.debug(etree.tostring(root))
	parseXmlToDictonary(root, '/', dictonaryItems, arrayGroups, filter)

## Method for parsing a xml-element to a dbus-object-path.
# The dbus-object-path can be a setting (contains a text-value) or
# a group. Only for a setting attributes are added.
# @param element The lxml-Element from the ElementTree API.
# @param path The path of the element.
# @param dictonaryItems The dictonary for the settings.
# @param arrayGroups The array for the groups.
# @param filter A filter used for filtering in settingchanges.xml for example "Add".
def parseXmlToDictonary(element, path, dictonaryItems, arrayGroups, filter):
	if path != '/':
		path += '/'
	path += element.tag
	for child in element:
		parseXmlToDictonary(child, path, dictonaryItems, arrayGroups, filter)

	if filter == None or path.startswith(filter) == True:
		if filter != None:
			objectPath = path.replace(filter, '')
			if objectPath == '':
				return
		else:
			objectPath = path

		# Remove possible underscore prefix
		objectPath = objectPath.replace("/_", "/")

		if element.get('type') != None:
			elementType = element.attrib[TYPE]
			text = element.text
			if not element.text:
				text = ''
			value = convertToType(elementType, text)
			dictonaryItems[objectPath] = [value, element.attrib]
		elif arrayGroups != None:
			if not objectPath in arrayGroups:
				arrayGroups.append(objectPath)

## Method for parsing a dictonary to a giving xml-file.
# The dictonary must be in following format {dbus-object-path, [value, {attributes}]}.
# @param dictonary The dictonary with the settings.
# @param file The filename.
def parseDictonaryToXmlFile(dictonary, file):
	logger.debug('parseDictonaryToXmlFile %s' % file)
	root = None
	for key in list(dictonary):
		items = key.split('/')
		items.remove('')
		if root == None:
			root = etree.Element(items[0])
			root.set(settingsTag, settingsVersion)
			tree = etree.ElementTree(root)
		items.remove(root.tag)
		elem = root
		for item in items:
			# Prefix items starting with a digit, because an XML element cannot start with a digit.
			if item[0].isdigit():
				item = "_" + item

			foundElem = elem.find(item)
			if foundElem == None:
				elem = etree.SubElement(elem, item)
			else:
				elem = foundElem
		elem.text = str(dictonary[key][VALUE])
		attributes = dictonary[key][ATTRIB]
		for attribute, value in attributes.iteritems():
			elem.set(attribute, str(value))
	newFile = file + newFileExtension
	with open(newFile, 'wb') as fp:
		tree.write(fp, encoding = settingsEncoding, pretty_print = True, xml_declaration = True)
		fp.flush()
		os.fsync(fp.fileno())

	try:
		rename(newFile, file)
	except:
		logger.error('renaming new file to settings file failed')

def to_bool(val):
	if type(val) != str:
		return bool(val)

	try:
		return bool(int(val))
	except:
		pass

	return val.lower() == 'true'

## Validate and convert to value + attributes
def makeDictEntry(val, itemtype, minval, maxval, silent):
	if itemtype not in supportedTypes:
		raise Exception('invalid type')

	val = convertToType(itemtype, val)
	attrs = { TYPE: itemtype, DEFAULT: str(val) }

	if type(val) != str:
		minval = convertToType(itemtype, minval)
		maxval = convertToType(itemtype, maxval)

		if minval or maxval:
			if val < minval or val > maxval:
				raise Exception('default value out of range')
			attrs[MIN] = str(minval)
			attrs[MAX] = str(maxval)

	silent = to_bool(silent)
	attrs[SILENT] = str(silent)

	return val, attrs

## Load settings from text file
def loadSettingsFile(name, dictionary):
	with open(name, 'r') as f:
		for line in f:
			v = re.sub('#.*', '', line).strip().split()
			if not v:
				continue

			try:
				path = v[0]
				defval = v[1]
				itemtype = v[2]
			except:
				raise Exception('syntax error: ' + line)

			minval = v[3] if len(v) > 3 else 0
			maxval = v[4] if len(v) > 4 else 0
			silent = v[5] if len(v) > 5 else 0

			val, attrs = makeDictEntry(defval, itemtype,
						   minval, maxval, silent)

			path = '/Settings/' + path.lstrip('/')
			dictionary[path] = [val, attrs]

## Load settings from each file in dir
def loadSettingsDir(path, dictionary):
	try:
		names = os.listdir(path)
	except:
		return

	for name in names:
		filename = os.path.join(path, name)
		try:
			loadSettingsFile(filename, dictionary)
		except Exception as ex:
			logger.error('error loading %s: %s' %
					  (filename, str(ex)))

## Handles the system (Linux / Windows) signals such as SIGTERM.
#
# Stops the logscript with an exit-code.
# @param signum the signal-number.
# @param stack the call-stack.
def handlerSignals(signum, stack):
	logger.warning('handlerSignals received: %d' % signum)
	exitCode = 0
	if signum == signal.SIGHUP:
		exitCode = 1
	exit(exitCode)

# Migration code.
# TODO ideally this should be elsewhere.
def delete_from_tree(tree, path):
	obj = tree.xpath(path)
	if not obj:
		return
	obj[0].getparent().remove(obj[0])

## Migrate old canbus settings
def migrate_can_profile(tree):
	global fileSettings
	global newFileSettings

	if not os.path.isfile("/etc/venus/canbus_ports"):
		return

	with open('/etc/venus/canbus_ports', 'r') as f:
		iflist = f.readline().split(None, 1)
		if not iflist:
			return
		interface = iflist[0]

	path = "/Settings/Canbus/" + interface + "/Profile"

	if tree.xpath(path):
		return

	# default to Ve.Can
	profile = 1

	if tree.xpath("/Settings/Services/LgResu/text()") == ["1"]:
		profile = 3
	elif tree.xpath("/Settings/Services/OceanvoltMotorDrive/text()") == ["1"] or \
		tree.xpath("/Settings/Services/OceanvoltValence/text()") == ["1"]:
		profile = 4
	elif tree.xpath("/Settings/Services/VeCan/text()") == ["0"]:
		profile = 0

	print("Setting " + path + " to " + str(profile))

	settings = tree.getroot()
	canbus = settings.find("Canbus")
	if canbus == None:
		canbus = etree.SubElement(settings, "Canbus")

	inter = canbus.find(interface)
	if inter == None:
		inter = etree.SubElement(canbus, interface)

	prof = etree.SubElement(inter, "Profile")
	prof.text = str(profile)
	prof.set('type', 'i')

	delete_from_tree(tree, "/Settings/Services/LgResu")
	delete_from_tree(tree, "/Settings/Services/OceanvoltMotorDrive")
	delete_from_tree(tree, "/Settings/Services/OceanvoltValence")
	delete_from_tree(tree, "/Settings/Services/VeCan")

	tree.write(newFileSettings, pretty_print = True, xml_declaration = True, encoding = settingsEncoding)
	rename(newFileSettings, fileSettings)

## The main function.
def run():
	global bus
	global dbusName
	global myDbusServices
	global myDbusGroupServices
	global settings
	global pathSettings
	global fileSettings
	global fileSettingChanges
	global newFileSettings
	global sysSettingsDir
	global groups
	global busName
	global tracingEnabled
	global pathTraces
	global traceToConsole
	global traceToFile
	global traceFileName
	global traceDebugOn
	global myDbusMainGroupService
	global logger

	DBusGMainLoop(set_as_default=True)

	# set the settings path
	fileSettings = pathSettings + fileSettings
	fileSettingChanges = pathSettings + fileSettingChanges
	newFileSettings = pathSettings + newFileSettings

	# setup debug traces.
	logger = logging.getLogger('localsettings victron applicaiton')
	#formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
	formatter = logging.Formatter('%(levelname)s - %(message)s')
	logger.setLevel(logging.INFO)

	if traceDebugOn == 1:
		logger.setLevel(logging.DEBUG)

	if traceToConsole == 1:
		print ("log console activated")
		ch = logging.StreamHandler()
		ch.setFormatter(formatter)
		ch.setLevel(logging.DEBUG)
		logger.addHandler(ch)

	if traceToFile == 1 :
		fh = logging.FileHandler('trace.log')
		fh.setFormatter(formatter)
		fh.setLevel(logging.DEBUG)
		print ("tracefilename:")
		print (traceFileName + traceFileName)
		logger.addHandler(fh)

	#tracing.setupTraces(tracingEnabled, pathTraces, traceFileName, traceToConsole, traceToFile, traceDebugOn)
	logger.debug('tracingPath = %s' % pathTraces)

	# Print the logscript version
	logger.info('Localsettings version is: 0x%04x' % version)
	logger.info('Localsettings PID is: %d' % getpid())

	# Trace the python version.
	pythonVersion = platform.python_version()
	logger.debug('Current python version: %s' % pythonVersion)

	# setup signal handling.
	signal.signal(signal.SIGHUP, handlerSignals) # 1: Hangup detected
	signal.signal(signal.SIGINT, handlerSignals) # 2: Ctrl-C
	signal.signal(signal.SIGUSR1, handlerSignals) # 10: kill -USR1 <logscript-pid>
	signal.signal(signal.SIGTERM, handlerSignals) # 15: Terminate

	# load system default settings
	loadSettingsDir(sysSettingsDir, settings)

	if not path.isdir(pathSettings):
		print('Error path %s does not exist!' % pathSettings)
		sys.exit(errno.ENOENT)

	if path.isfile(newFileSettings):
		logger.info('New settings file exist')
		try:
			tree = etree.parse(newFileSettings)
			root = tree.getroot()
			logger.info('New settings file %s validated' % newFileSettings)
			rename(newFileSettings, fileSettings)
			logger.info('renamed new settings file to settings file')
		except:
			logger.error('New settings file %s invalid' % newFileSettings)
			remove(newFileSettings)
			logger.error('%s removed' % newFileSettings)

	if path.isfile(fileSettings):
		# Try to validate the settings file.
		try:
			tree = etree.parse(fileSettings)
			root = tree.getroot()
			migrate_can_profile(tree)
			logger.info('Settings file %s validated' % fileSettings)
		except:
			logger.error('Settings file %s invalid' % fileSettings)
			remove(fileSettings)
			logger.error('%s removed' % fileSettings)

	# check if settings file is present, if not exit create a "empty" settings file.
	if not path.isfile(fileSettings):
		logger.warning('Settings file %s not found' % fileSettings)
		root = etree.Element(settingsRootName)
		root.set(settingsTag, settingsVersion)
		tree = etree.ElementTree(root)
		tree.write(fileSettings, encoding = settingsEncoding, pretty_print = True, xml_declaration = True)
		logger.warning('Created settings file %s' % fileSettings)

	# read the settings.xml
	parseXmlFileToDictonary(fileSettings, settings, groups, None)
	logger.debug('settings:')
	logger.debug(settings.items())
	logger.debug('groups:')
	logger.debug(groups)

	# check if new settings must be changed
	if path.isfile(fileSettingChanges):
		# process the settings which must be deleted.
		delSettings = {}
		parseXmlFileToDictonary(fileSettingChanges, delSettings, None, "/Change/Delete")
		logger.debug('setting to delete:')
		logger.debug(delSettings.items())
		for item in delSettings:
			if item in settings:
				logger.debug('delete item %s' % item)
				del settings[item]
				saveChanges = True

		# process the settings which must be added.
		addSettings = {}
		parseXmlFileToDictonary(fileSettingChanges, addSettings, None, "/Change/Add")
		logger.debug('setting to add:')
		logger.debug(addSettings.items())
		saveChanges = False
		for item in addSettings:
			if not item in settings:
				logger.debug('add item %s' % item)
				settings[item] = addSettings[item]
				saveChanges = True

		if saveChanges == True:
			logger.warning('Change settings according to %s' % fileSettingChanges)
			parseDictonaryToXmlFile(settings, fileSettings)
			# update settings and groups from file.
			settings = {}
			groups = []
			parseXmlFileToDictonary(fileSettings, settings, groups, None)
			logger.debug('settings:')
			logger.debug(settings.items())
			logger.debug('groups:')
			logger.debug(groups)
			remove(fileSettingChanges)

	# For a PC, connect to the SessionBus
	# For a CCGX, connect to the SystemBus
	bus = dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in environ else dbus.SystemBus()
	busName = dbus.service.BusName(dbusName, bus)
	root = RootObject(busName)

	for setting in settings:
		myDbusObject = MyDbusObject(busName, setting)
		myDbusServices.append(myDbusObject)
	for group in groups:
		myDbusObject = MyDbusObject(busName, group)
		myDbusGroupServices.append(myDbusObject)
	myDbusMainGroupService = myDbusGroupServices[0]

	MainLoop().run()

def usage():
	print("Usage: ./localsettings [OPTION]")
	print("-h, --help\tdisplay this help and exit")
	print("-t\t\tenable tracing to file (standard off)")
	print("-d\t\tset tracing level to debug (standard info)")
	print("-v, --version\treturns the program version")
	print("--banner\tshows program-name and version at startup")
	print ("--verbose\tSpeak like my mother")
	print("--path=dir\tuse given dir as data directory instead of /data")
	print("")
	print("NOTE FOR DEBUGGING ON DESKTOP")
	print("This code expects a path /data/conf or --path to be set, and")
	print("permissions in that path to write/read.")

def main(argv):
	global tracingEnabled
	global traceToConsole
	global traceToFile
	global traceDebugOn
	global pathSettings
	global timeoutSaveSettingsTime

	try:
		opts, args = getopt.getopt(argv, "vhctd", ["help", "version", "banner", "path=", "no-delay","verbose"])
	except getopt.GetoptError:
		usage()
		sys.exit(errno.EINVAL)
	for opt, arg in opts:
		if opt == '-h' or opt == '--help':
			usage()
			sys.exit()
		elif opt == '-t':
			tracingEnabled = True
			traceToFile = True
		elif opt == '-d':
			traceDebugOn = True
		elif opt == '--verbose':
			tracingEnabled = True
			traceToConsole = True
		elif opt == '--path':
			pathSettings = arg
			if pathSettings[-1] != '/':
				pathSettings += "/"
		elif opt == '--no-delay':
			print("no delay")
			timeoutSaveSettingsTime = 0
		elif (opt == '-v' or opt == '--version'):
			print(version)
			sys.exit()

	print("localsettings v%01x.%02x starting up " % (FIRMWARE_VERSION_MAJOR, FIRMWARE_VERSION_MINOR))

	run()

main(sys.argv[1:])


import sklearn.datasets
from sklearn import linear_model
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt

cancer = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(
    cancer.data, cancer.target, stratify=cancer.target, random_state=202)

# Training set score: 0.92 for C = 0.001
# Test set score: 0.92 for C = 0.001

# Training set score: 0.96 for C = 1
# Test set score: 0.94 for C = 1

# Training set score: 0.97 for C = 100
# Test set score: 0.96 for C = 100

for C, marker in zip([0.001, 1, 100], ['o', '^', 'v']):
    lr_l1 = linear_model.LogisticRegression(C=C, penalty='l2', solver='liblinear').fit(X_train, y_train)
    print("Training set score: {:.2f} for C = {}".format(lr_l1.score(X_train, y_train), C))
    print("Test set score: {:.2f} for C = {}".format(lr_l1.score(X_test, y_test), C))
    plt.plot(lr_l1.coef_.T, marker, label="C={}".format(C))

plt.xticks(range(cancer.data.shape[1]), cancer.feature_names, rotation=90)
plt.hlines(0, 0, cancer.data.shape[1])
plt.ylim(-5, 5)
plt.xlabel("Coefficient index")
plt.ylabel("Coefficient magnitude")
plt.legend()
plt.show()


import urllib.request
import urllib.parse
import simplejson
import tweepy
import string
import re

class User:
	def __init__(self, userID):
		self.id = userID

	def getHomeTimeline(self):
		auth = tweepy.OAuthHandler(Tw_consumerInfo(), Tw_consumerInfo("secret"))
		if Tw_getKey(self.id) != 'error' and Tw_getKey(self.id, 'secret') != 'error':
			auth.set_access_token(Tw_getKey(self.id), Tw_getKey(self.id, 'secret'))
			api = tweepy.API(auth)
			statuses = tweepy.Cursor(api.home_timeline).items(20)
			tweets = []
			for s in statuses:
				string1 = ''
				for c in s.text:
					if c in letters:
						string1 = string1 + c
					else:
						string1 = string1 + '(?)'
				tweets.append(string1)
			return tweets
		else:
			return 'error'

	def getCalendarEvents(self):#	Google Api
		url3 = 'https://www.googleapis.com/calendar/v3/calendars/primary/events'
		if self.id < userNumber():
			try:
				a_token_h = "Bearer " + clientToken(self.id)
				headers = {}
				headers['Authorization'] = a_token_h
				req = urllib.request.Request(url3, headers=headers)
				resp = urllib.request.urlopen(req)

				#simplejson.load(response) converters url response data into python data (dicts, lists, strings)
				json = simplejson.load(resp)
				events = json["items"]
				elist = []
				for event in events:
					stuff = {}
					stuff["start"] = event["start"]["dateTime"]
					stuff["end"] = event["end"]["dateTime"]
					stuff["title"] = event['summary']
					elist.append(stuff)
				return elist
			except:
				try:
					refreshAccess(self.id)
					a_token_h = "Bearer " + clientToken(self.id)
					headers = {}
					headers['Authorization'] = a_token_h
					req = urllib.request.Request(url3, headers=headers)
					resp = urllib.request.urlopen(req)
					json = simplejson.load(resp)
					events = json["items"]
					elist = []
					for event in events:
						stuff = {}
						stuff["start"] = event["start"]["dateTime"]
						stuff["end"] = event["end"]["dateTime"]
						stuff["title"] = event['summary']
						elist.append(stuff)
					return elist
				except Exception as e:
					print(str(e))
					return None

#_____________________Google Api__________________________
def refreshAccess(userID):
	url2 = 'https://www.googleapis.com/oauth2/v4/token'

	values2 = {'client_id' : clientInfo(),
				'client_secret' : clientInfo('secret'),
				'refresh_token' : clientToken(userID, 'r'),
				'grant_type' : 'refresh_token'}

	try:
		data = urllib.parse.urlencode(values2)
		data = data.encode('utf-8')
		req = urllib.request.Request(url2, data)
		resp = urllib.request.urlopen(req)
		resp_data = str(resp.read())

		a_token = ''.join(re.findall(r'"access_token": "(.*?)"', resp_data))
		writeAToken(a_token, userID)
	except Exception as e:
		print(str(e))

def clientToken(userID, type='a'):
	fob = open('secret_doc.txt', 'r')
	lines = fob.readlines()
	fob.close()
	if userID > userNumber():
		return 'Error'
	if type is 'r':
		r_token = ''.join(re.findall(r'(.*?)\n', lines[2*(userID + 1) + 1]))
		return r_token
	else:
		a_token = ''.join(re.findall(r'(.*?)\n', lines[2*(userID+ 1)]))
		return a_token

def writeAToken(key, userID):
	fob = open('secret_doc.txt', 'r')
	lines = fob.readlines()
	fob.close()
	while len(lines) < (2*(userID + 1) + 2):
		lines.append("\n")
	lines[2*(userID+ 1)] = key + "\n"
	fob = open('secret_doc.txt', 'w')
	fob.writelines(lines)
	fob.close()

def clientInfo(type='id'):
	fob = open('secret_doc.txt', 'r')
	lines = fob.readlines()
	fob.close()
	if type is 'secret':
		client_secret = ''.join(re.findall(r'client_secret: (.*?)\n', lines[1]))
		return client_secret
	else:
		client_id = ''.join(re.findall(r'client_id: (.*?)\n', lines[0]))
		return client_id

#_____________________Twitter Api__________________________
def Tw_getKey(user, type=None):
	fob = open('Tw_secret.txt', 'r')
	lines = fob.readlines()
	fob.close()
	if user >= userNumber():
		return 'error'
	if type is 'secret':
		secret = ''.join(re.findall(r'(.*?)\n', lines[2*(user + 1) + 1]))
		return secret
	else:
		token = ''.join(re.findall(r'(.*?)\n', lines[2*(user + 1)]))
		return token

def Tw_consumerInfo(type=None):
	fob = open('Tw_secret.txt', 'r')
	lines = fob.readlines()
	fob.close()
	if type is 'secret':
		consumer_secret = ''.join(re.findall(r'consumer_secret: (.*?)\n', lines[1]))
		return consumer_secret
	else:
		consumer_key = ''.join(re.findall(r'consumer_key: (.*?)\n', lines[0]))
		return consumer_key

letters = string.printable + 'äöå'
#____________________________________________________________

def userNumber(api='g'):
	if api == 'Tw':
		fob = open('Tw_secret.txt', 'r')
	else:
		fob = open('secret_doc.txt', 'r')
	lines = fob.readlines()
	fob.close()
	number = int((len(lines) - 2)/2)
	return number

if __name__ == '__main__':
    print("This is a module.")


#!/usr/bin/env python3

import sys
import logging
import random

logging.basicConfig(level=logging.INFO)

def check_num(arr):
    index = 0
    while index < len(arr):
        if type(arr[index] != float):
            return False
        index += 1

    return True

def get_solution(arr):
    if check_num(arr) is False:
        check = False
        no_copy = list(set(arr))
        arr = sorted(no_copy)

        if len(arr) == 0:
            return True

        if len(arr) == 1:
            return arr[0]

        if check is False:
            return arr[1]

    return True

def main():
    items = []
    index = 0
    while index < 60:
        a = round(random.uniform(0, 100), 2)
        items.append(a)
        index += 1

    logging.info(items)

    if get_solution(items) is True:
        logging.info("Something wrong with array")
        sys.exit(-1)

    item = str(get_solution(items))
    logging.info("Solution: %s", item)

if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        logging.info("До встречи!")


from selenium import webdriver

class WebdriverFactory:
    def __init__(self,browser):
        self.browser=browser

    def getWebDriverInstance(self):
        baseURL = 'https://github.com/'
        if self.browser == 'firefox':
            driver=webdriver.Firefox(executable_path=r"C:\Chandransh\Pycharm_Projects\Infogix_CodingTest\driver\geckodriver.exe")
        elif self.browser == "iexplorer":
            driver=webdriver.Ie(executable_path=r"C:\Chandransh\Pycharm_Projects\Infogix_CodingTest\driver\IEDriverServer.exe")
        elif self.browser == "chrome":
            driver=webdriver.Chrome(executable_path=r"C:\Chandransh\Pycharm_Projects\Infogix_CodingTest\driver\chromedriver.exe")
        else:
            driver=webdriver.Chrome(executable_path=r"C:\Chandransh\Pycharm_Projects\Infogix_CodingTest\driver\chromedriver.exe")

        driver.maximize_window()
        driver.implicitly_wait(3)
        driver.get(baseURL)
        return driver


#!/bin/env python
# -*- coding: utf-8 -*-
import mesh
import fem
import laplacian
import splitter
from math import cos,sin,pi,sqrt
from scipy import sparse
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.tri as tri
import VisuSplitMesh as VSM
import VisuSolution as VS
from scipy.sparse import linalg as sp_linalg

def g(x,y) :
    return cos(2*pi*x)+sin(2*pi*y)

m = mesh.read("CarreMedium.msh")
coords    = m[0]
elt2verts = m[1]
nbVerts = coords.shape[0]
nbElts  = elt2verts.shape[0]
print('nbVerts : {}'.format(nbVerts))
print('nbElts  : {}'.format(nbElts))
begVert2Elts, vert2elts = mesh.compvert2elts(elt2verts)

begRows, indCols = fem.comp_skel_csr_mat(elt2verts, (begVert2Elts, vert2elts) )
nz = begRows[-1]
print("Number of non zero in sparse matrix : {}".format(nz))

spCoefs = np.zeros( (nz,), np.double)

for iElt in range(nbElts):
    iVertices = elt2verts[iElt,:]
    crd1 = coords[iVertices[0],:]
    crd2 = coords[iVertices[1],:]
    crd3 = coords[iVertices[2],:]
    matElem = laplacian.comp_eltmat((crd1[0],crd1[1]), (crd2[0],crd2[1]),
                                    (crd3[0],crd3[1]))
    fem.add_elt_mat_to_csr_mat((begRows,indCols,spCoefs),
                               (iVertices, iVertices, matElem))

# Assemblage second membre :
f = np.zeros(nbVerts, np.double)
for iVert in range(nbVerts):
    if ( coords[iVert,3] > 0 ) :
        f[iVert] += g(coords[iVert,0],coords[iVert,1])
b = np.zeros(nbVerts, np.double)
for i in range(nbVerts) :
    for ptR in range(begRows[i],begRows[i+1]):
        b[i] -= spCoefs[ptR]*f[indCols[ptR]]

# Il faut maintenant tenir compte des conditions limites :
for iVert in range(nbVerts):
    if coords[iVert,3] > 0: # C'est une condition limite !
        # Suppression de la ligne avec 1 sur la diagonale :
        for i in range(begRows[iVert],begRows[iVert+1]):
            if indCols[i] != iVert :
                spCoefs[i] = 0.
            else :
                spCoefs[i] = 1.
        # Suppression des coefficients se trouvant sur la colonne iVert :
        for iRow in range(nbVerts):
            if iRow != iVert :
                for ptCol in range(begRows[iRow],begRows[iRow+1]):
                    if indCols[ptCol] == iVert :
                        spCoefs[ptCol] = 0.

        b[iVert] = f[iVert]
# On definit ensuite la matrice :
spMatrix = sparse.csc_matrix((spCoefs, indCols, begRows),
                             shape=(nbVerts,nbVerts))
#print("Matrice creuse : {}".format(spMatrix))

# Visualisation second membre :
VS.view( coords, elt2verts, b, title = "second membre" )

# Résolution du problème séquentiel avec gradient conjugue :
iteration = 1
def display_iter(x_k):
    global iteration
    print("iteration {:03d} : ||A.xk-b||/||b|| = {}".format(iteration, np.linalg.norm(spMatrix * x_k - b)/np.linalg.norm(b)))
    iteration += 1

# Résoud à l'aide du gradient conjugué.
sol,info = sp_linalg.cg(spMatrix, b, x0=None, tol=1.E-14, M=None,callback=display_iter)
#print(sol)
d = spMatrix.dot(sol) - b
print("||A.x-b||/||b|| = {}".format(sqrt(d.dot(d)/b.dot(b))))
# Visualisation de la solution :
VS.view( coords, elt2verts, sol, title = "Solution" )

nbDoms = 8
ndsDomains = splitter.node( nbDoms, coords )
print("Nombre de noeuds par domaines : ")
i = 0
for a in ndsDomains :
    print("Domaine {} : {} sommets\n".format(i,a.shape[0]))
    i += 1

etsDomains = splitter.element( nbDoms, (elt2verts, coords) )
print("Nombre d'elements par domaines : ")
i = 0
for a in etsDomains :
    print("Domaine {} : {} elements\n".format(i,a.shape[0]))
    i += 1

elt2doms = np.zeros((nbElts,), np.double)
ia = 0.
for a in etsDomains :
    for e in a :
        elt2doms[e] = ia
    ia += 1

# Calcul l'interface :
ie = 0
mask = np.array([-1,]*nbVerts, np.short)
for e in elt2verts :
    d = elt2doms[ie]
    if mask[e[0]] == -1 :
        mask[e[0]] = d
    elif mask[e[0]] != d :
        mask[e[0]] = -2
    if mask[e[1]] == -1 :
        mask[e[1]] = d
    elif mask[e[1]] != d :
        mask[e[1]] = -2
    if mask[e[2]] == -1 :
        mask[e[2]] = d
    elif mask[e[2]] != d :
        mask[e[2]] = -2
    ie += 1

nbInterf = 0
for m in mask :
    if m == -2 :
        nbInterf += 1

interfNodes = np.empty(nbInterf, np.long)
nbInterf = 0
for im in range(mask.shape[0]):
    if mask[im] == -2 :
        interfNodes[nbInterf] = im
        nbInterf += 1

VSM.view( coords, elt2verts, nbDoms, elt2doms, indInterfNodes = interfNodes, title='Partition par elements')

vert2dom = np.zeros((nbVerts,), np.double)
ia = 0.
for a in ndsDomains :
    for v in a :
        vert2dom[v] = ia
    ia += 1

mask = np.zeros((nbVerts,), np.short)
for e in elt2verts :
    d1 = vert2dom[e[0]]
    d2 = vert2dom[e[1]]
    d3 = vert2dom[e[2]]
    if (d1 != d2) or (d1 != d3) or (d2 != d3) :
        mask[e[0]] = 1
        mask[e[1]] = 1
        mask[e[2]] = 1

nbInterf = 0
for m in mask :
    if m == 1 :
        nbInterf += 1
interfNodes = np.empty(nbInterf, np.long)
nbInterf = 0
for im in range(mask.shape[0]):
    if mask[im] == 1 :
        interfNodes[nbInterf] = im
        nbInterf += 1

VSM.view( coords, elt2verts, nbDoms, vert2dom, indInterfNodes = interfNodes, title='Partition par sommets')


# this script will create MultiPatch feature classes from a polygon and DSM/DTM Raster.
# These rasters acn also be automatically derived from LAS or LASD in this process.
# Original Process & concept by Joseph McGlinchy & (intern name)
# LAS & LASD Integration, interpolateAdditionalPoints algorithm, & optimization by Geoff Taylor
# Enjoy :)

import arcpy
import os
import os.path
import tempfile
import glob
from arcpy.sa import *

arcpy.env.overwriteOutput = True
arcpy.CheckOutExtension('spatial')
arcpy.CheckOutExtension('3d')

#################
# Define Inputs #
#################

ProductionMode = False  # Set to True for ArcGIS Pro GP Tool Use
if ProductionMode:
    ''' For ArcGIS PRO GP Tool '''
    inLASD = arcpy.GetParameterAsText(0)
    inLAS = arcpy.GetParameterAsText(1)
    DTMRaster = arcpy.GetParameterAsText(2)
    DSMRaster = arcpy.GetParameterAsText(3)
    buildingFootprints = arcpy.GetParameterAsText(4)
    sr = arcpy.GetParameterAsText(5)  # Spatial Reference
    outputWS = arcpy.GetParameterAsText(6)
    interpolateAdditionalPoints = arcpy.GetParameterAsText(7)
    recursivelyCreateAndClipRastersFromLasd = arcpy.GetParameterAsText(8)
    scratchGDB = arcpy.env.scratchGDB
    tempFolder = tempfile.mkdtemp()

else:
    ''' For Testing Purposes'''  # Comment out inputs
    inLASD = r'' #C:\Users\geof7015\PycharmProjects\LiDARTestData\testData\LiDAR\backup\LASD\test.lasd' #C:\Users\geof7015\PycharmProjects\LiDARTestData\testData\LiDAR\backup\LASD\test.lasd'  # C:\Users\geof7015\PycharmProjects\testData\Charlotte\LiDAR\LiDARAOI.lasd'
    inLAS = r'C:\Users\geof7015\PycharmProjects\testData\Boulder\LAS\329.las'     #C:\Users\geof7015\PycharmProjects\testData\Charlotte\LiDAR'  #C:\Users\geof7015\PycharmProjects\LiDARTestData\testData\LiDAR' # C:\Users\geof7015\PycharmProjects\LiDARTestData\testData\LiDAR13810E4750N.las # C:\Users\geof7015\PycharmProjects\LiDARTestData\testData #  C:\Users\geof7015\PycharmProjects\LiDARTestData\testData\LiDAR13810E4750N.las #  r'C:\Users\geof7015\PycharmProjects\testData\Charlotte\LiDAR'  # C:\Users\geof7015\PycharmProjects\testData\Charlotte\LiDAR
    DTMRaster = r''  # C:\workspace\data\testdata\bh12TVK1800084000.img
    DSMRaster = r''  # 'C:\workspace\data\testdata\hh12TVK1800084000.img'
    buildingFootprints = r'C:\Users\geof7015\PycharmProjects\testData\Boulder\Data.gdb\Building329'     # C:\Users\geof7015\PycharmProjects\testData\Charlotte\Data.gdb\BldgFootprints'
    sr = "PROJCS['NAD_1983_HARN_StatePlane_Colorado_North_FIPS_0501_Feet',GEOGCS['GCS_North_American_1983_HARN',DATUM['D_North_American_1983_HARN',SPHEROID['GRS_1980',6378137.0,298.257222101]],PRIMEM['Greenwich',0.0],UNIT['Degree',0.0174532925199433]],PROJECTION['Lambert_Conformal_Conic'],PARAMETER['False_Easting',3000000.000316083],PARAMETER['False_Northing',999999.999996],PARAMETER['Central_Meridian',-105.5],PARAMETER['Standard_Parallel_1',39.71666666666667],PARAMETER['Standard_Parallel_2',40.78333333333333],PARAMETER['Latitude_Of_Origin',39.33333333333334],UNIT['Foot_US',0.3048006096012192]]"
    # "PROJCS['NAD_1983_StatePlane_North_Carolina_FIPS_3200_Feet',GEOGCS['GCS_North_American_1983',DATUM['D_North_American_1983',SPHEROID['GRS_1980',6378137.0,298.257222101]],PRIMEM['Greenwich',0.0],UNIT['Degree',0.0174532925199433]],PROJECTION['Lambert_Conformal_Conic'],PARAMETER['False_Easting',2000000.002616666],PARAMETER['False_Northing',0.0],PARAMETER['Central_Meridian',-79.0],PARAMETER['Standard_Parallel_1',34.33333333333334],PARAMETER['Standard_Parallel_2',36.16666666666666],PARAMETER['Latitude_Of_Origin',33.75],UNIT['Foot_US',0.3048006096012192]]"
    outputWS = r'C:\Users\geof7015\PycharmProjects\testData\Boulder\Workspace.gdb'
    scratchGDB = arcpy.env.scratchGDB
    tempFolder = tempfile.mkdtemp()
    # TODO Geof7015 resolve bug with Boundary Clean Raster Int to Float issue!
    # interpolateAdditionalPoints currently broken
    interpolateAdditionalPoints = False
    recursivelyCreateAndClipRastersFromLasd = False

########################
# Set Global Variables #
########################

global lasList
global DTM
global DSM
global LrDSM
global ptFileInfoFile
global ptFileInfoList
global ptSpacing
global avgPtSpacing

##################
# Define Modules #
##################
''' place all code modules here and notate each well...'''

def createlasdataset(inLAS, sr):
    global inLASD
    inLASD = os.path.join(tempFolder, "LASDataSet.lasd")
    if arcpy.Exists(inLASD):
        arcpy.Delete_management(inLASD)
    arcpy.CreateLasDataset_management(inLAS, inLASD, False, "", sr, "COMPUTE_STATS")
    if arcpy.Exists(inLASD):
        arcpy.AddMessage("LASD File Created @ Location: " + inLASD)
        return inLASD
        # for multiples:  return inLASD,output2,output3,etc...
    else:
        arcpy.AddMessage("Could Not Create LASD DataSet. Check LAS inputs for errors")

def findLasDatasetStatisticsfilePaths(file):
    file_object = open(file, 'r')
    lines = file_object.readlines()
    file_object.close()
    cleanLines = []
    for line in lines:
        if len(line) > 1:
            path = line.split(",")[0]
            if os.path.isabs(path) is True and path not in cleanLines:
                cleanLines.append(path)
    return cleanLines

# Create Lists with LiDAR Statistical Information.  Pt Spacing etc... Process only used in other modules.
def obtainLiDARInfo(inLASD,lasList):
    if arcpy.Exists(inLASD):
        arcpy.AddMessage("Calculating Necessary Statistics for Feature Extraction Process")
        lasDatasetStatsText = os.path.join(tempFolder, "lasDatasetStatsText.txt")
        if arcpy.Exists(lasDatasetStatsText):
            arcpy.Delete_management(lasDatasetStatsText)
        arcpy.LasDatasetStatistics_management(inLASD, "true", lasDatasetStatsText, "LAS_FILES", "COMMA",
                                              "DECIMAL_POINT")

        # TODO DJARRARD obtain a LiDAR file from list and parse the point_spacing to building footprints.
        # TODO DJARRARD if multiple LiDAR tiles overlap building footprints then point_spacing = pt_spacing_average
        if recursivelyCreateAndClipRastersFromLasd:
            pass

        if not recursivelyCreateAndClipRastersFromLasd:
            # run arcpy.PointFileInfo_3d on the single tile (no recursion)
            ptFileInfoFile = os.path.join(outputWS, 'ptFileInfoFile')
            if arcpy.Exists(ptFileInfoFile):
                arcpy.Delete_management(ptFileInfoFile)
            arcpy.PointFileInformation_3d(lasList, ptFileInfoFile, "LAS", None, sr, "false", "false", "DECIMAL_POINT",
                                          "false", "false")

            rows = arcpy.SearchCursor(ptFileInfoFile,
                                      fields="FileName; Pt_Spacing; Z_Min; Z_Max",
                                      sort_fields="FileName; Pt_Spacing; Z_Min; Z_Max")
                # Iterate through the rows in the cursor and store the
            # "FileName; Pt_Spacing; Z_Min; Z_Max"
            ptFileInfoList = []
            PtSpacing = []
            for row in rows:
                formattedfields = ("{0}, {1}, {2}, {3}".format(
                    row.getValue("FileName"),
                    row.getValue("Pt_Spacing"),
                    row.getValue("Z_Min"),
                    row.getValue("Z_Max")))
                ptFileInfoList.append(formattedfields)
                ptspacinglist = float("{0}".format(
                    row.getValue("Pt_Spacing")))
                PtSpacing.append(ptspacinglist)
            print(ptFileInfoList)
            print(PtSpacing)
            avgPtSpacing = sum(PtSpacing)/float(len(PtSpacing))
            print(avgPtSpacing)
            return ptFileInfoFile, ptFileInfoList, PtSpacing, avgPtSpacing

# TODO Geof7015 & DJARRARD Integrate automated class code detection for feature extraction process for .las or .lasd use

def createlaslayers(inLASD):
    # Something like def createLasDatasetLayer(inLASD, #DTMLASDclasses, #DTMLASDreturns, #LrDSMLASDclasses, #LrDSMLASDreturns, interpolateAdditionalPoints):
    if arcpy.Exists("DTMLASD"):
        arcpy.Delete_management("DTMLASD")
    if arcpy.Exists("LRDSMLASD"):
        arcpy.Delete_management("LRDSMLASD")
    arcpy.MakeLasDatasetLayer_management(inLASD, "DTMLASD", "2", "", "", "", "", "", "", "")
    arcpy.MakeLasDatasetLayer_management(inLASD, "LRDSMLASD", "2;6", "", "", "", "", "", "", "")
    if arcpy.Exists("DTMLASD"):
        if arcpy.Exists("LRDSMLASD"):
            arcpy.AddMessage("LASD Layers Created")
    else:
        arcpy.AddMessage("Could Not Create LASD Layers")

def createSurfaceRasters(lasdLayerGround, lasdLayerSurface, outputDTM, outputDSM):
    # selector determining whether or not to interpolateAdditional points for tin creation. Helps with Terribe LiDAR.
    ''' if interpolateAdditionalPoints is enabled then input correct heightValue & valueField for raster processing '''
    ''' Determine the correct point spacing settings based on raster processing algorithm requirements '''
    if interpolateAdditionalPoints:
        if not recursivelyCreateAndClipRastersFromLasd:
            pointSpacing = obtainLiDARInfo(inLASD, lasList)[3]  # return 3 is Average LiDAR point Spacing
        else:
            # TODO make point spacing support recursions. obtainLiDARInfo(inLASD, lasList)[3] is currently placeholder
            ''' calculate average pt spacing of LiDAR tiles building footprints intersect'''
            pointSpace = obtainLiDARInfo(inLASD, lasList)[3]
            pointSpacing = pointSpace / 0.5
        heightValue = 100
        valueField = "INT"
    else:
        if not recursivelyCreateAndClipRastersFromLasd:
            pointSpacing = obtainLiDARInfo(inLASD, lasList)[3]
        else:
            # TODO make point spacing support recursions.  obtainLiDARInfo(inLASD, lasList)[3] is currently placeholder
            ''' calculate average pt spacing of LiDAR tiles building footprints intersect'''
            pointSpacing = obtainLiDARInfo(inLASD, lasList)[3]
        heightValue = 1
        valueField = "FLOAT"

    # Delete terrain rasters if existing.
    if arcpy.Exists(outputDTM):
        arcpy.Delete_management(outputDTM)
    if arcpy.Exists(outputDSM):
        arcpy.Delete_management(outputDSM)

    arcpy.LasDatasetToRaster_conversion(lasdLayerGround, outputDTM, "ELEVATION", "BINNING MAXIMUM NATURAL_NEIGHBOR",
                                        valueField, "CELLSIZE", pointSpacing, heightValue)
    print("Created DTM Raster at location: " + outputDTM)
    arcpy.LasDatasetToRaster_conversion(lasdLayerSurface, outputDSM, "ELEVATION", "BINNING MAXIMUM NATURAL_NEIGHBOR",
                                        valueField, "CELLSIZE", pointSpacing, heightValue)
    print("Created  Last Return DSM Raster at location: " + outputDSM)
    return outputDTM, outputDSM

# TODO INT.  make sure that integer raster is being produced
def interpolateBetweenLasPts(DTM,LrDSM):
    # Raster Cleanup Process via Boundary Clean Operation (Fills & Smooths Voids based on nearest pixel value)
    DTMBC = os.path.join(scratchGDB, "DTMBC")
    DTMBoundaryClean = arcpy.sa.BoundaryClean(DTM, "ASCEND", "TWO_WAY")
    DTMBoundaryClean.save(DTMBC)
    LrDSMBC = os.path.join(scratchGDB, "LrDSMBC")
    LrDSMBoundaryClean = arcpy.sa.BoundaryClean(LrDSM, "ASCEND", "TWO_WAY")
    LrDSMBoundaryClean.save(LrDSMBC)

    # TODO INT.  check and see id produced raster is created correctly
    # Divide rasters by 1000 for accurate Height & Override the Original DEM & DSM Raster Inputs with the LiDAR Ext ones
    DTMFinal = arcpy.Raster(DTMBC) / 100  # May need to resolve output rasters format to true Float
    DTM = os.path.join(scratchGDB, "DTM")
    DTMFinal.save(DTM)
    LrDSMFinal = arcpy.Raster(LrDSMBC) / 100  # May need to resolve output rasters format to true Float
    DSM = os.path.join(scratchGDB, "DSM")
    LrDSMFinal.save(DSM)
    print("Complete Creation of Interpolation Data")
    return DTM, DSM

def createNDSM(DSM, DTM, nDSM):
    temp = arcpy.Raster(DSM) - arcpy.Raster(DTM)
    # TODO Geof7015 obtain feet or meters from SR as input for automated unit selector function
    ''' then pass conversion equation case units == meters: minBldgHeight * 0.3048 else: minBldgHeight '''
    minBldgHeight = 6   # Value is in Feet
    nDSMRaster = SetNull(temp < minBldgHeight, temp)
    nDSMRaster.save(nDSM)
    del temp
    return nDSM

def maskDSM(DSM,maskRaster,DSMMasked):
    # Mask DSM to nDSM height limiting value
    DSMMaskOperation = arcpy.sa.ExtractByMask(DSM, maskRaster)
    DSMMaskOperation.save(DSMMasked)
    print("DSMMasked File Location" + DSMMasked)
    print("DTM File Location" + DTM)
    return DSMMasked

##############
# Begin Code #
##############
''' commence Code/Script operation here and notate well...'''

# Conditional operation to allow for input of .lasd file, .las file folder or Rasters
''' Detects if  dataSet exists and runs the correct operation based on dataSet'''

# If LiDAR Input is a LASD DataSet then count number of LAS files in LAS Dataset and List LAS files as input 4 GP tools
if arcpy.Exists(inLASD):
    arcpy.AddMessage("detected LASD Dataset as input: " + inLASD)
    lasDatasetStatsText = os.path.join(tempFolder, "lasDatasetStatsText.txt")
    arcpy.LasDatasetStatistics_management(inLASD, "true", lasDatasetStatsText, "LAS_FILES", "COMMA", "DECIMAL_POINT")
    filenames = findLasDatasetStatisticsfilePaths(lasDatasetStatsText)

    if len(filenames) == 0:
        arcpy.AddMessage("1 LAS file detected in LASD DATASET")
    else:
        arcpy.AddMessage("{0} LAS files detected in LASD DATASET".format(len(filenames)))

    # Process lasList into Esri GP tool friendly input format
    newstr = str(filenames)[1:-1].replace("', ", ";")
    lasList = '"' + newstr.replace("'", "") + '"'

# If the LiDAR Input is a a single LAS then return 1 of LAS files and format file to string for GP tools input.
if inLAS.lower().endswith('.las') and ";" not in inLAS:
    arcpy.AddMessage("1 LAS file detected")
    lasList = '"' + inLAS + '"'

# If the LiDAR Input is a string of LAS files then count number of LAS files and create List LAS files input 4 GP tools.
if inLAS.lower().endswith('.las') and ";" in inLAS:
    numberLASFiles = (inLAS.count(';')+1)
    arcpy.AddMessage(str(numberLASFiles) + " LAS file detected")
    lasList = '"' + inLAS + '"'

# If the LiDAR Input is a LAS Directory then count number of LAS files and create List of LAS files as input 4 GP tools.
if os.path.isdir(inLAS):
    lasSearchPathDirectory = inLAS + "/*.las"
    for name in glob.glob(lasSearchPathDirectory):
        filename = name
        file_extension = ".las"
        filename, file_extension = os.path.splitext(name)
        if file_extension == ".las":
            # Find all LAS files in input folder. Optionally search recursively
            recursive = True
            lasList = []
            if recursive:
                for root, dirs, files in os.walk(inLAS):
                    for file in files:
                        if file.endswith(".las") and file not in lasList:
                            lasList.append((os.path.join(root, file)))
            else:
                for file in os.listdir(inLAS):
                    if file.endswith(".las") and file not in lasList:
                        lasList.append((os.path.join(inLAS, file)))

    # Print Number of Las files
    if len(lasList) == 0:
        arcpy.AddMessage("1 LAS file detected in Directory")
    else:
        arcpy.AddMessage("{0} LAS files detected in Directory".format(len(lasList)))

    # Process lasList into Esri GP tool friendly input format
    newstr = str(lasList)[1:-1].replace("', ", ";")
    lasList = '"' + newstr.replace("'", "") + '"'

# Convert Las file List as String and format for GP tool input
# Create LASDataset from LAS files.
if inLAS.lower().endswith('.las') or os.path.isdir(inLAS):
    createlasdataset(inLAS=inLAS, sr=sr)

if inLAS.lower().endswith('.las') or os.path.isdir(inLAS) or arcpy.Exists(inLASD):
    createlaslayers(inLASD=inLASD)
    if not recursivelyCreateAndClipRastersFromLasd:
        LrDSM = os.path.join(scratchGDB, "LrDSM")
        DTM = os.path.join(scratchGDB, "DTM")
        createSurfaceRasters(lasdLayerGround="DTMLASD", lasdLayerSurface="LRDSMLASD", outputDTM=DTM, outputDSM=LrDSM)
        arcpy.Delete_management("DTMLASD")
        arcpy.Delete_management("LRDSMLASD")
    else:
        pass

# TODO INT Make sure that INT raster is produced
# Run raster interpolation algorithm on LiDAR derived rasters if interpolateAdditionalPoints is True and not Recursive
if inLAS.lower().endswith('.las') or os.path.isdir(inLAS) or arcpy.Exists(inLASD):
    if interpolateAdditionalPoints and not recursivelyCreateAndClipRastersFromLasd:
            interpolateBetweenLasPts(DTM, LrDSM)
    else:
        DSM = os.path.join(outputWS, "LrDSM")

# Process Rasters prior to recursion process if Raster as user Input or recursivelyCreateAndClipRastersFromLasd deselect
if (arcpy.Exists(DTMRaster) and arcpy.Exists(DSMRaster)) or not recursivelyCreateAndClipRastersFromLasd:

    # Change names of initial input rasters to DTM and DSM to align with LiDAR derived rasters for less coding :)
    if arcpy.Exists(DTMRaster) and arcpy.Exists(DSMRaster):
        DTM = DTMRaster
        DSM = DSMRaster

    # Begin process of deriving rasters from created LAS DataSets
    if inLAS.lower().endswith('.las') or os.path.isdir(inLAS) or arcpy.Exists(inLASD):
        pass
        # get spatial reference
        sr = arcpy.Describe(DTM).spatialReference

        DSM = LrDSM
        nDSM = os.path.join(scratchGDB, "nDSM")
        createNDSM(DSM=DSM, DTM=DTM, nDSM=nDSM)

        DSMMaskedRaster = os.path.join(scratchGDB, "DSMMaskedRaster")
        maskDSM(DSM=DSM, maskRaster=nDSM, DSMMasked=DSMMaskedRaster)

        DSM = DSMMaskedRaster

# create list for multiPatch features
mp_list = []

# make search cursor for footprint polygons
fields = ["SHAPE@"]
with arcpy.da.SearchCursor(buildingFootprints, fields) as sc:
    for i, row in enumerate(sc):
        try:
            print("on feature {0}".format(i))
            # get raster extent
            geom = row[0]
            ext = "{0} {1} {2} {3}".format(geom.extent.XMin, geom.extent.YMin, geom.extent.XMax, geom.extent.YMax)

            # copy the feature temporarily
            tp = os.path.join(outputWS, "tp")
            tempGeom = arcpy.CopyFeatures_management(geom, tp)

            # clip the DSM
            print('clipping DSM')
            DSMClipRast = os.path.join(arcpy.env.scratchFolder, 'tempclip{0}.tif'.format(i))
            arcpy.Clip_management(DSM, ext, DSMClipRast, tp, "true", "false")

            if interpolateAdditionalPoints:
                # Int Raster
                DSMClipRast = Int(DSMClipRast)

            # smooth the DSM
            print('smoothing DSM')
            nbr = NbrRectangle(3, 3, "CELL")

            # sm_clip = FocalStatistics(out_raster, nbr, "MEAN", "DATA")
            sm_clip = Filter(DSMClipRast, "LOW", "DATA")

            # clean up clipped raster
            arcpy.Delete_management(DSMClipRast)

            # TODO INT Make sure that Raster Points are produced correctly and at precise elevations
            # convert raster to points
            print('converting raster to points')
            out_points = "in_memory/clipPoints"
            arcpy.RasterToPoint_conversion(sm_clip, out_points, "Value")

            # convert to 3d by values
            # out_points3d ="in_memory/clipPoints3d"
            # arcpy.FeatureTo3DByAttribute_3d(in_features=out_points, out_feature_class=out_points3d,
            # height_field="grid_code", to_height_field="")

            # TODO INT Make sure that TIN Surface is produced correctly and at precise elevation
            # Create TIN with points
            print('making surface TIN')
            # feats_tin = "{} Shape.Z Mass_Points <None>;".format(out_points3d)
            feats_tin = "{0} grid_code Mass_Points <None>;".format(out_points)
            out_surf_tin = os.path.join(arcpy.env.scratchFolder, "surfTin")
            arcpy.CreateTin_3d(out_surf_tin, sr, feats_tin, 'DELAUNAY')

            # clip the DTM
            print('clipping DTM')
            dtmClipRast = os.path.join(arcpy.env.scratchFolder, 'tempDEMclip{0}.tif'.format(i))
            arcpy.Clip_management(DTM, ext, dtmClipRast, tp, "true", "false")

            # TODO INT  Check to ensure whether or not this is necessary
            # convert DEM to Int
            if interpolateAdditionalPoints:
                dtmClipRast = Int(dtmClipRast)

            # add Min Height to Building Footprints
            print('determining Minimum Building Elevation')
            arcpy.AddField_management(tp, "ID", "SHORT", None, None, None, "ID", "true", "true", None)
            arcpy.CalculateField_management(tp, "ID", 1, "PYTHON_9.3", None)
            minMaxElevTable = "in_memory/minMaxElev"
            arcpy.sa.ZonalStatisticsAsTable(tp, "ID", dtmClipRast, minMaxElevTable, "true", "MIN_MAX_MEAN")
            arcpy.JoinField_management(tp, "ID", minMaxElevTable, "ID", "MIN;MAX")

            # then, move building footprints to MIN Z Height
            out_poly3d = "in_memory/out_poly3d"
            arcpy.FeatureTo3DByAttribute_3d(tp, out_poly3d, "MIN", "")

            # make ground TIN
            gnd_feats_tin = "{} Shape.Z Hard_Clip <None>;".format(out_poly3d)
            out_gnd_tin = os.path.join(arcpy.env.scratchFolder, "gndTin")
            arcpy.CreateTin_3d(out_gnd_tin, sr, gnd_feats_tin, "DELAUNAY")

            # extrude polygon between TINs
            print('creating Multipatch')
            this_MP = os.path.join(outputWS, "bldgMP_{0}".format(i))
            arcpy.ExtrudeBetween_3d(out_surf_tin, out_gnd_tin, out_poly3d, this_MP)

            # add feature name to list
            mp_list.append(this_MP)

            # Delete Unnecessary files
            arcpy.Delete_management(tp)
            arcpy.Delete_management(out_points)
            arcpy.Delete_management(minMaxElevTable)
            arcpy.Delete_management(out_poly3d)

            # TODO Geoff7015 Incorporate Cleanup Building CGA from Geof7015 rule into tool here:
            ''' every multipatch building must have LiDAR point spacing as a attribute and "Units: feet/meters
                will need to update CGA cleanup rules settings with a conditional calculator operation where
                it leverages these attributes and changes the cleanupGeometry operations optimally based on input features
                final output will be two file geodatabases. one with original buildings and other with cleaned.'''
            ''' Other Cleanup Utility tools/processes may be required to optimize building faces and roof geometries'''
        except:
            print("Unable to process feature {0}".format(i))

# merge the MultiPatches into a single FC
outputMerge = os.path.join(outputWS, 'outputMergeMP')
arcpy.Merge_management(mp_list, outputMerge)

#TODO DJARRARD: delete all buildingMP* files that exist in the output workspace
# Delete Individual Multipatch Buildings
'''if arcpy.Exists(os.path.join(outputWS, "bldgMP_0")):
    for fc in arcpy.ListFeatureClasses("bldgMP*", "MULTIPATCH", outputWS):
        arcpy.Delete_management(fc)'''

if arcpy.Exists("DTMLASD"):
    arcpy.Delete_management("DTMLASD")
if arcpy.Exists("LRDSMLASD"):
    arcpy.Delete_management("LRDSMLASD")


#!/usr/bin/python3

from which_pyqt import PYQT_VER

if PYQT_VER == 'PYQT5':
    from PyQt5.QtCore import QLineF, QPointF
elif PYQT_VER == 'PYQT4':
    from PyQt4.QtCore import QLineF, QPointF
else:
    raise Exception('Unsupported Version of PyQt: {}'.format(PYQT_VER))

from copy import copy, deepcopy
import traceback
import time
import numpy as np
from TSPClasses import *
import heapq
import itertools
import traceback

class Stack(object):

    def __init__(self):
        self.items = []

    def push(self, item):
        self.items.append(item)

    def pop(self):
        return self.items.pop()

    def peek(self):
        return self.items[-1]

    def isEmpty(self):
        return len(self.items) == 0

class TSPSolver:
    def __init__(self, gui_view):
        self._scenario = None

    def setupWithScenario(self, scenario):
        self._scenario = scenario

    ''' <summary>
		This is the entry point for the default solver
		which just finds a valid random tour.  Note this could be used to find your
		initial BSSF.
		</summary>
		<returns>results dictionary for GUI that contains three ints: cost of solution,
		time spent to find solution, number of permutations tried during search, the
		solution found, and three null values for fields not used for this
		algorithm</returns>
	'''

    def defaultRandomTour(self, time_allowance=60.0):
        results = {}
        cities = self._scenario.getCities()
        ncities = len(cities)
        foundTour = False
        count = 0
        bssf = None
        start_time = time.time()
        while not foundTour and time.time() - start_time < time_allowance:
            # create a random permutation
            perm = np.random.permutation(ncities)
            route = []
            # Now build the route using the random permutation
            for i in range(ncities):
                route.append(cities[perm[i]])
            bssf = TSPSolution(route)
            count += 1
            if bssf.cost < np.inf:
                # Found a valid route
                foundTour = True

        end_time = time.time()
        results['cost'] = bssf.cost if foundTour else math.inf
        results['time'] = end_time - start_time
        results['count'] = count
        results['soln'] = bssf
        results['max'] = None
        results['total'] = None
        results['pruned'] = None
        return results

    ''' <summary>
		This is the entry point for the greedy solver, which you must implement for
		the group project (but it is probably a good idea to just do it for the branch-and
		bound project as a way to get your feet wet).  Note this could be used to find your
		initial BSSF.
		</summary>
		<returns>results dictionary for GUI that contains three ints: cost of best solution,
		time spent to find best solution, total number of solutions found, the best
		solution found, and three null values for fields not used for this
		algorithm</returns>
	'''

    def greedy(self, time_allowance=60.0):
        try:
            # initialization is O(n) space to hold all the cities and O(1) time
            bssf = None
            cities = self._scenario.getCities()
            self.num_cities = len(cities)
            solution_dict = {}
            start_time = time.time()
            # stay under the time limit
            while time.time() - start_time < time_allowance:
                # for each city as the starting node
                for index_loop in range(len(cities)):
                    print("Starting with city {}".format(index_loop))
                    city = cities[index_loop]
                    city_path = []
                    city_path.append(city)
                    to_visit = deepcopy(cities)
                    current_city = city
                    del to_visit[index_loop]
                    while len(to_visit):
                        city_costs = self.get_closest_cities(current_city, to_visit)
                        closest_city_tuple = city_costs[0]
                        closest_city_index = to_visit.index(closest_city_tuple[0])
                        closest_city = to_visit[closest_city_index]
                        if not self._scenario._edge_exists[current_city._index][closest_city._index]:
                            # should only happen if all costs to remaining cities are inf
                            break
                        del to_visit[closest_city_index]
                        city_path.append(closest_city)
                        current_city = closest_city
                    if len(to_visit):
                        continue
                    else:
                        bssf = TSPSolution(city_path)
                        end_time = time.time()
                        results = {}
                        results['cost'] = bssf.cost
                        results['time'] = end_time - start_time
                        results['count'] = None
                        results['soln'] = bssf
                        results['max'] = None
                        results['total'] = None
                        results['pruned'] = None
                        solution_dict[index_loop] = results
                        continue

                self.lowest_cost = float("inf")
                for key, solution in solution_dict.items():
                    print(key, solution["cost"])
                    if solution["cost"] < self.lowest_cost:
                        self.lowest_cost = solution["cost"]
                        lowest = solution

                print("BSSF is {}".format(lowest["soln"].cost))
                return lowest

        except Exception as e:
            print(e)
            traceback.print_exc()
            raise (e)

    def get_distance_matrix(self, city_list):
        ### create and initialize the matrix ###
        matrix = np.full((len(city_list), len(city_list)), fill_value=np.inf)
        for from_index, city in enumerate(city_list):
            for dest_index, dest_city in enumerate(city_list):
                if from_index == dest_index:
                    # already init to inf
                    continue
                dist = city.costTo(dest_city)
                matrix[from_index][dest_index] = dist

        return matrix

    ''' <summary>
		This is the entry point for the branch-and-bound algorithm that you will implement
		</summary>
		<returns>results dictionary for GUI that contains three ints: cost of best solution,
		time spent to find best solution, total number solutions found during search (does
		not include the initial BSSF), the best solution found, and three more ints:
		max queue size, total number of states created, and number of pruned states.</returns>
	'''

    """
	ALGORITHM ANALYSIS:
	O(k*n^2) time where k is the number of states generated and checked
	O(k*n^2) space where k is the number of states generated and on the heap

	IMPLEMENTATION HELP:
	tuples are ordered as following in min heap
	0. value that will be used for sorting
	1. the current node to visit and expand
	2. the nodes that have yet to  be visited
	3. the current reduced matrix for that sub problem
	4. The order of the nodes visited
	5. The total cost of this subproblem
	"""

    def branchAndBound(self, time_allowance=60.0):
        try:
            # initialization is O(n) space to hold all the cities and O(1) time
            bssf = self.greedy(time_allowance=time_allowance)['soln']
            # Random too slow
            # bssf2 = self.defaultRandomTour(time_allowance=time_allowance)['soln']
            # print("BSSF1 {} and BSSF2 {}".format(bssf.cost, bssf2.cost))
            # bssf = bssf if min(bssf.cost, bssf2.cost) == bssf.cost else bssf2
            print("Greedy and Random Algorithms found a BSSF of {}".format(bssf.cost))
            cities = self._scenario.getCities()
            self.cities = cities
            self.lowest_ave_cost = float("inf")
            self.num_cities = len(cities)
            heap = []
            bssf_updates = 0
            max_queue_len = 1
            total_states_created = 1
            pruned_subproblems = 0
            num_solutions = 0
            self.lowest_cost = bssf.cost
            # getting a recuced matrix is O(n^2) time and space
            initial_reduced_matrix, lower_bound = self.get_init_reduced_matrix(cities)
            print("Initial lower bound is {}".format(lower_bound))
            starting = tuple((lower_bound,
                              cities[0],
                              cities[1:],
                              initial_reduced_matrix,
                              [cities[0]._index],
                              lower_bound))
            heapq.heappush(heap, starting)
            time_start = time.time()
            # stay under the time limit
            while time.time() - time_start < time_allowance and len(heap):
                # find the best solutions
                # pop is O(logn) time to re-heapify and O(1) space
                next_to_try = heapq.heappop(heap)
                # check is O(1) time and space
                if next_to_try[5] < self.lowest_cost:
                    # for city in cities to visit still
                    # print("Exanding subproblem with cost {} and {} more cities to visit".format(next_to_try[5],
                    # 																			len(next_to_try[2])))
                    for city in next_to_try[2]:
                        # MAKE SURE PATH EXISTS
                        if self._scenario._edge_exists[next_to_try[1]._index][city._index]:
                            # getting reduced matrix is O(n^2) time and space
                            new_subproblem = self.get_reduced_matrix(city, next_to_try[3], next_to_try)
                            # if there are no more nodes to visit
                            if not len(new_subproblem[2]):
                                # this is O(n) to get the cities
                                route = self.index_to_cities(new_subproblem[4])
                                # checks and creation are O(1) time and space
                                bssf = TSPSolution(route)
                                if bssf.cost < self.lowest_cost:
                                    self.lowest_cost = min(bssf.cost, self.lowest_cost)
                                    bssf_updates += 1
                                num_solutions += 1
                                print("### Found a solution!!! Score: {} ####".format(self.lowest_cost))
                            # There are still nodes to visit
                            else:
                                # should we prune this node?
                                if new_subproblem[5] < self.lowest_cost:
                                    # It is O(logn) time to heapify after inserting
                                    # O(1) space
                                    heapq.heappush(heap, new_subproblem)
                                    total_states_created += 1
                                else:
                                    pruned_subproblems += 1
                                    total_states_created += 1

                else:
                    pruned_subproblems += 1
                    total_states_created += 1
                # checking is just comparing two numbers, O(1) time and space
                max_queue_len = max(len(heap), max_queue_len)

            # all of these are O(1) time and space to print and return
            end_time = time.time()
            print("BSSF updates: {}".format(bssf_updates))
            results = {}
            results['cost'] = self.lowest_cost
            results['time'] = end_time - time_start
            results['count'] = num_solutions
            results['soln'] = bssf
            results['max'] = max_queue_len
            print("The max length of the queue is {}".format(max_queue_len))
            results['total'] = total_states_created
            print("The total number of nodes created was {}".format(total_states_created))
            results['pruned'] = pruned_subproblems
            print("The total number of nodes pruned was {}".format(pruned_subproblems))

            return results
        except Exception as e:
            # error catching so I can see errors
            print(e)
            print(traceback.format_exc())
            raise (e)

    """
	This involves only a lookup and a divide so it is O(1) time and space to get
	the length of the array and divide
	"""

    def get_value(self, score, cities_visited):
        if len(cities_visited):
            # note I tried a lot of these to get varying results
            return score / len(cities_visited)
        else:
            # we will add this to bssf anyways
            return 0

    """
	This function is O(n^2) time and space to visit and update every cell and return an array of
	size O(n^2). More details are in the comments.
	"""

    def get_reduced_matrix(self, next_city_to_visit, matrix, given_tuple):
        # copy the tuple so it doesn't make changes to the main one
        # these are O(1) time and O(n^2) space to copy the matrix
        tuple_of_items = deepcopy(given_tuple)
        matrix = matrix.copy()
        sum_to_reduce = 0
        initial_cost_to_city = matrix[tuple_of_items[1]._index][next_city_to_visit._index]

        # since we're visiting the row and column, make them inf
        # O(1) time and space
        matrix[tuple_of_items[1]._index] = np.inf
        matrix[:, next_city_to_visit._index] = np.inf
        matrix[next_city_to_visit._index][tuple_of_items[1]._index] = np.inf

        # The following code runs over all the rows and then all the columns.  This is
        # O(n^2) where n is the number of cities since it updates every part of the matrix
        # it is O(1) space since it does it with the same matrix

        # do rows and get mins
        for row in range(matrix.shape[0]):
            min_for_row = np.min(matrix[row])
            if np.isinf(min_for_row):
                continue
            matrix[row] = matrix[row] - min_for_row
            sum_to_reduce += min_for_row

        # get reduced columns now
        for col in range(matrix.shape[1]):
            min_for_col = np.min(matrix[:, col])
            if np.isinf(min_for_col):
                continue
            matrix[:, col] = matrix[:, col] - min_for_col
            sum_to_reduce += min_for_col

        # This is O(n) time to visit all the cities and O(1) space to delete it
        cities_to_still_visit = tuple_of_items[2]
        cities_to_still_visit = self.delete_city_by_id(cities_to_still_visit, next_city_to_visit)

        new_cost = tuple_of_items[5] + initial_cost_to_city + sum_to_reduce

        ### Create the tuple to store in the queue: this is O(n^2) space to hold the matrix
        return_tuple = ((self.get_value(tuple_of_items[5], tuple_of_items[4]),  # the value for queue
                         next_city_to_visit,  # the city object that was visited now
                         cities_to_still_visit,  # the nodes to still visit
                         matrix,  # the new distance matrix
                         tuple_of_items[4] + [next_city_to_visit._index],  # the order of nodes visited
                         new_cost))  # the new cost

        return return_tuple

    """
	This is a O(n^2) algorithm to get the distance from all nodes to every other node
	Also O(n^2) space to hold the matrix
	"""

    def get_init_reduced_matrix(self, city_list):
        ### create and initialize the matrix ###
        matrix = np.full((len(city_list), len(city_list)), fill_value=np.inf)
        for from_index, city in enumerate(city_list):
            for dest_index, dest_city in enumerate(city_list):
                if from_index == dest_index:
                    # already init to inf
                    continue
                dist = city.costTo(dest_city)
                matrix[from_index][dest_index] = dist

        # The following options are O(n^2) time to operate on every cell
        # it is O(1) space
        sum_to_reduce = 0
        # do rows and get mins
        for row in range(matrix.shape[0]):
            min_for_row = np.min(matrix[row])
            matrix[row] = matrix[row] - min_for_row
            sum_to_reduce += min_for_row

        # get reduced columns now
        for col in range(matrix.shape[1]):
            min_for_col = np.min(matrix[:, col])
            matrix[:, col] = matrix[:, col] - min_for_col
            sum_to_reduce += min_for_col

        return matrix, sum_to_reduce

    """
	This is O(1) time and space to do a multiplication and a subtraction
	This function is seperate so that it can be changed easily
	"""

    def get_cost_from_value(self, new_subproblem):
        value = new_subproblem[0]
        num_cities_left = len(new_subproblem[2])
        return value  ## / np.square(self.num_cities - num_cities_left)

    """"
	This function turns all the indexes into their City class counterpart
	It is O(n) time and space to iterate through and hold all the cities
	"""

    def index_to_cities(self, city_indices):
        city_list = []
        for city_int in city_indices:
            city_list.append(self.cities[city_int])

        return city_list

    """
	This is just a print debug function.  O(1) time and space
	"""

    def print_subprob(self, subprob):
        print("Node: {}, current_cost: {}".format(subprob[1]._index, subprob[5]))

    """
	This function goes through the list and delete the node based on it's ID
	It is O(n) time to go through every node worst case and O(1) space
	"""

    def delete_city_by_id(self, cities_to_still_visit, next_city_to_visit):
        for index, city in enumerate(cities_to_still_visit):
            if city._index == next_city_to_visit._index:
                delete_index = index
                break

        del cities_to_still_visit[delete_index]
        return cities_to_still_visit

    """
	Used for greedy algorithm
	This is O(n) time and space to find the closest city and to store the list and find the min
	"""

    def get_closest_cities(self, city, city_list):
        cost = {}
        for city_to_visit in city_list:
            cost[city_to_visit] = city.costTo(city_to_visit)

        sorted_x = sorted(cost.items(), key=lambda kv: kv[1])
        # print("Closest length is {}".format(sorted_x[0][1]))
        return sorted_x

    ''' <summary>
		This is the entry point for the algorithm you'll write for your group project.
		</summary>
		<returns>results dictionary for GUI that contains three ints: cost of best solution,
		time spent to find best solution, total number of solutions found during search, the
		best solution found.  You may use the other three field however you like.
		algorithm</returns>
	'''
    def fancy(self, time_allowance=60.0):
        try:
            start_node_num = 0
            cost = {}
            city_list = self._scenario.getCities()
            start_city = city_list[start_node_num]
            start_time = time.time()
            for city_to_visit in city_list:
                cost[frozenset([city_to_visit._index])] = {"cost": start_city.costTo(city_to_visit),
                                                           "prev": city_to_visit._index}

            cities_without_start = [number for number in range(len(city_list)) if number != start_node_num]
            while time.time() - start_time < time_allowance:
                for subset_length in range(2, len(city_list)):
                    combinations_list = list(itertools.combinations(cities_without_start, subset_length))
                    for combination in combinations_list:
                        min_value = float("inf")
                        min_prev = None
                        for city_index, city_num in enumerate(combination):
                            subset = list(combination)
                            del subset[city_index]
                            lookup = cost[frozenset(subset)]
                            value = lookup["cost"] + city_list[lookup["prev"]].costTo(city_list[city_num])
                            if value <= min_value:
                                min_value = value
                                min_prev = city_num
                        cost[frozenset(combination)] = {"cost": min_value, "prev": min_prev}

                # do last iteration
                min_value = float("inf")
                min_prev = None
                # try to see distance to initial city from all other cities
                for city_index, city_num in enumerate(cities_without_start):
                    subset = list(cities_without_start)
                    del subset[city_index]
                    lookup = cost[frozenset(subset)]
                    # final cost is cost from city_num as 2nd to last coming from the rest of the subset
                    # and going to the zero-th node
                    value = lookup["cost"] + city_list[lookup["prev"]].costTo(city_list[city_num]) + \
                            city_list[city_num].costTo(city_list[0])
                    if value <= min_value:
                        min_value = value
                        min_prev = city_num
                cost[frozenset(city_list)] = {"cost": min_value, "prev": min_prev}

                # trace pointer back
                route = []
                # this is the last node
                current_prev_pointer = cost[frozenset(city_list)]["prev"]
                current_cities_set = set(cities_without_start)
                # go until 2nd to last node (otherwise this method doesn't work)
                while len(current_cities_set) > 1:
                    # insert node into route list
                    route.insert(0, city_list[current_prev_pointer])
                    # get set without that city
                    current_cities_set = current_cities_set - set([current_prev_pointer])
                    current_prev_pointer = cost[frozenset(current_cities_set)]["prev"]

                # add last element of set - 2nd to beginning node
                route.insert(0, city_list[current_cities_set.pop()])
                # add start city
                route.insert(0, city_list[start_node_num])
                # return solution
                bssf = TSPSolution(route)
                end_time = time.time()
                results = {}
                results['cost'] = bssf.cost
                results['time'] = end_time - start_time
                results['count'] = None
                results['soln'] = bssf
                results['max'] = None
                results['total'] = None
                results['pruned'] = None
                return results

        except Exception as e:
            # error catching so I can see errors
            print(e)
            print(traceback.format_exc())
            raise (e)


#!/usr/bin/env python3
"""
Counting DNA nucleotides
Usage: ./dna.py [input file]
"""

import sys
from tools import check_input

def n_count(dna_string):
    """Count the number of occurrences of "A", "C", "G" and "T" in dna_string."""
    a_count = 0
    c_count = 0
    g_count = 0
    t_count= 0
    for nuc in dna_string:
        if nuc.upper() == 'A':
            a_count += 1
        elif nuc.upper() == 'C':
            c_count += 1
        elif nuc.upper() == 'G':
            g_count += 1
        elif nuc.upper() == 'T':
            t_count += 1
        else:
            continue
    print(a_count, c_count, g_count, t_count)

def main():
    """Count input nucleotides."""
    check_input(sys.argv[0])
    with open(sys.argv[1]) as infile:
        seq = infile.readline().strip()
        n_count(seq)