#!/usr/bin/env python
# Copyright 2015 Jo Pol
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see http://www.gnu.org/licenses/.

from __future__ import division
from math import pi, sin, cos, tan, radians


# We will use the inkex module with the predefined 
# Effect base class.
import inkex, simplestyle

__author__ = 'Jo Pol'
__credits__ = ['Veronika Irvine','Jo Pol','Mark Shafer']
__license__ = 'GPLv3'

class PolarGrid(inkex.Effect):
	"""
	Creates a dotted polar grid where distance between the circles 
	increase with the distance between the dots on the circles
	"""
	def __init__(self):
		"""
		Constructor.
		"""

		# Call the base class constructor.
		inkex.Effect.__init__(self)

		# parse the options
		self.OptionParser.add_option('-a', '--angle', action='store', type='float', dest='angleOnFootside', default=45, help='grid angle (degrees)')
		self.OptionParser.add_option('-d', '--dots', action='store', type='int', dest='dotsPerCircle', default=180, help='number of dots on a circle')
		self.OptionParser.add_option('-o', '--outerDiameter', action='store', type='float', dest='outerDiameter', default=160, help='outer diameter (mm)')
		self.OptionParser.add_option('-i', '--innerDiameter', action='store', type='float', dest='innerDiameter', default=100, help='minimum inner diameter (mm)')
		self.OptionParser.add_option('-f', '--fill', action='store', type='string', dest='dotFill', default='-6711040', help='dot color')
		self.OptionParser.add_option('-A', '--alignment', action='store', type='string', dest='alignment', default='outside', help='exact diameter on [inside|outside]')
		self.OptionParser.add_option('-s', '--size', action='store', type='float', dest='dotSize', default=0.5, help='dot diameter (mm)')
		self.OptionParser.add_option('-v', '--variant', action='store', type='string', dest='variant', default='', help='omit rows to get [|rectangle|hexagon1]')
		self.OptionParser.add_option('-u', '--units', action = 'store', type = 'string', dest = 'units', default = 'mm', help = 'The units the measurements are in')

	def group(self, diameter):
		"""
		Create a group labeled with the diameter
		"""
		label = 'diameter: {0:.2f} mm'.format(diameter)
		attribs = {inkex.addNS('label', 'inkscape'):label}
		return inkex.etree.SubElement(self.gridContainer, inkex.addNS('g', 'svg'), attribs)

	def dots(self, diameter, circleNr, group):
		"""
		Draw dots on a grid circle
		"""
		offset = (circleNr % 2) * 0.5
		for dotNr in range (0, self.options.dotsPerCircle):
			a = (dotNr + offset) * self.alpha
			x = (diameter / 2.0) * cos(a)
			y = (diameter / 2.0) * sin(a)
			attribs = {'style':self.dotStyle, 'cx':str(x * self.scale), 'cy':str(y * self.scale), 'r':self.dotR}
			inkex.etree.SubElement(group, inkex.addNS('circle', 'svg'), attribs)

	def getUnittouu(self, param):
		" compatibility between inkscape 0.48 and 0.91 "
		try:
			return inkex.unittouu(param)
		except AttributeError:
			return self.unittouu(param)

	def getColorString(self, longColor, verbose=False):
		""" Convert the long into a #RRGGBB color value
			- verbose=true pops up value for us in defaults
			conversion back is A + B*256^1 + G*256^2 + R*256^3
		"""
		if verbose: inkex.debug("%s ="%(longColor))
		longColor = long(longColor)
		if longColor <0: longColor = long(longColor) & 0xFFFFFFFF
		hexColor = hex(longColor)[2:-3]
		hexColor = '#' + hexColor.rjust(6, '0').upper()
		if verbose: inkex.debug("  %s for color default value"%(hexColor))
		return hexColor

	def iterate(self, diameter, circleNr):
		"""
		Create a group with a ring of dots.
		Returns half of the arc length between the dots
		which becomes the distance to the next ring.
		"""
		group = self.group(diameter)
		self.dots(diameter, circleNr, group)
		self.generatedCircles.append(group)
		return diameter * self.change

	def generate(self):
		"""
		Generate rings with dots, either inside out or outside in
		"""
		circleNr = 0
		flag_error = False
		minimum = 2 * self.options.dotSize * self.options.dotsPerCircle /pi
		if minimum < self.options.innerDiameter:
			minimum = self.options.innerDiameter
		else:
			flag_error = True
		if self.options.alignment == 'outside':
			diameter = self.options.outerDiameter
			while diameter > minimum:
				diameter -= self.iterate(diameter, circleNr)
				circleNr += 1
		else:
			diameter = minimum
			while diameter < self.options.outerDiameter:
				diameter += self.iterate(diameter, circleNr)
				circleNr += 1
		# Display message
		if flag_error:
			# Leave message on top
			font_height = 8
			text_style = { 'font-size': str(font_height),
							'font-family': 'sans-serif',
							'text-anchor': 'middle',
							'text-align': 'center',
							'fill': '#000000' }
			text_atts = {'style':simplestyle.formatStyle(text_style),
						'x': '0', 'y': '0'}
			text = inkex.etree.SubElement(self.gridContainer, 'text', text_atts)
			text.text = "Dots overlap. inner changed to %4.1f" % (minimum)

	def removeGroups(self, start, increment):
		"""
		Remove complete rings with dots
		"""
		for i in range(start, len(self.generatedCircles), increment):
			self.current_layer.remove(self.generatedCircles[i])

	def removeDots(self, i, offset, step):
		"""
		Remove dots from one circle
		"""
		group = self.generatedCircles[i]
		dots = list(group)
		start = len(dots) - 1 - offset
		for j in range(start, -1, 0-step):
			group.remove(dots[j])

	def computations(self, angle):
		self.alpha = radians(360.0 / self.options.dotsPerCircle)
		correction = pi / (4 * self.options.dotsPerCircle)
		correction *= tan(angle*0.93)
		self.change = tan(angle - correction) * pi / self.options.dotsPerCircle

	def effect(self):
		"""
		Effect behaviour.
		Overrides base class' method and draws something.
		"""

		# color
		self.options.dotFill = self.getColorString(self.options.dotFill)
		# constants
		self.dotStyle = simplestyle.formatStyle({'fill': self.options.dotFill,'stroke':'none'})
		self.scale = self.getUnittouu("1" + self.options.units)
		self.dotR = str(self.options.dotSize * (self.scale/2))
		self.computations(radians(self.options.angleOnFootside))

		# processing variables
		self.generatedCircles = []
		self.gridContainer =  self.current_layer

		self.generate()

		if self.options.variant == 'rectangle':
			self.removeGroups(1, 2)
		elif self.options.variant == 'hexagon1':
			self.removeGroups(0, 3)
		elif self.options.variant == 'hexagon2' or self.options.variant == 'snow2':
			for i in range(0, len(self.generatedCircles), 1):
				self.removeDots(i, (((i%2)+1)*2)%3, 3)
		elif self.options.variant == 'hexagon3':
			for i in range(0, len(self.generatedCircles), 2):
				self.removeDots(i, (i//2+1)%2, 2)
		elif self.options.variant == 'hexagon4':
			self.removeGroups(0, 4)
		elif self.options.variant == 'hexagon5' or self.options.variant == 'snow1':
			for i in range(0, len(self.generatedCircles), 2):
				self.removeDots(i, 1, 2)

		self.dotStyle = simplestyle.formatStyle({'fill': 'none','stroke':self.options.dotFill,'stroke-width':0.7})
		self.dotR = str((((self.options.innerDiameter * pi) / self.options.dotsPerCircle) / 2) * self.scale)
		self.generatedCircles = []
		if self.options.variant == 'snow2':
			self.options.dotsPerCircle = self.options.dotsPerCircle // 3
			self.computations(radians(self.options.angleOnFootside))
			self.generate()
		elif self.options.variant == 'snow1':
			self.generate()
			self.removeGroups(1, 2)
			for i in range(0, len(self.generatedCircles), 2):
				self.removeDots(i, i%4, 2)
			for i in range(0, len(self.generatedCircles), 2):
				self.removeDots(i, (i+1)%2, 2)
			for i in range(2, len(self.generatedCircles), 4):
				self.removeDots(i, 0, self.options.dotsPerCircle)

# Create effect instance and apply it.
if __name__ == '__main__':
	PolarGrid().affect()