- Details
- Parent Category: Programming Assignments' Solutions
We Helped With This Python Programming Assignment: Have A Similar One?

Category | Programming |
---|---|
Subject | Python |
Difficulty | Undergraduate |
Status | Solved |
More Info | Python Assignment |
Short Assignment Requirements
Assignment Description
CSCI 1310 - Intro to Computer Programming
Instructor: Hoenigman Final Project
Due Sunday, December 4 by 5 pm.
How do you feel, on Twitter
Sentiment analysis is the process of computationally identifying a writer’s attitude towards a topic expressed in a piece of text. Some companies apply sentiment analysis to opinions expressed in social media about their products.
In this assignment, we are providing you with all tweets generated in the second week of November and you are going to use that data to generate a
geographic visualization of the sentiment expressed about particular topics. As an example, consider the following map that shows how people feel about Justin Bieber using the sentiments expressed in their tweets. States that are red have the most positive view, while states that are dark blue have the most negative view; yellow represents a more neutral view, while states in gray have insufficient data.
Figure 1. ... style='font-size:10.0pt;line-height:107%;font-family:"Times New Roman",serif; color:#4E81BD'>
To generate this image, thousands of tweets that included the word “bieber” were collected. Each tweet contained the latitude and longitude of the tweet’s location, which could be used to associate the tweet with a state. To determine if the tweet was overall positive or negative, the individual words in the tweet were analyzed. Words were assigned a score between -1 and +1 using a pre-defined dictionary of word sentiments. For example, a few of the words in the dictionary and their scores include,
'DEPLORABLE' = -1.0
'BAD' = -0.625
'GOOD' = 0.875
'EXCELLENT' = 1.0
If a word of the tweet is not found in the sentiment dictionary, it is ignored. The overall sentiment of the tweet is the average of the sentiment scores that are found. If no sentiment scores are found for any of the words of the tweet, this tweet is ignored. The overall sentiment of a state is computed as the average sentiment score for all tweets that are associated with that state (ignoring those tweets that did not have a sentiment score). The state's sentiment score is then mapped to a color between blue (negative) and red (positive) using a prescribed color gradient.
Data provided
There is a file on Moodle called tweets.zip that includes nine json files of tweets collected using the Twitter API. Some of the files have a timestamp, while others do not have a timestamp. All of the files contain the text in the tweet and the latitude and longitude of the tweeter.
Code provided
There are several files provided in finalProjectFiles.zip that provide the functionality for calculating the sentiment from the tweet text and graphically rendering the sentiment for each state. The files include,
• geo.py contains a GeoPosition class to represent a geographic location in terms of latitude and longitude. Each tweet will have a latitude and longitude that can be used to get its location relative to the states. State descriptions also have a latitude and longitude. Also included in GeoPosition is a distance method that computes that properly computes the shortest distance between two geographic locations (based on the distance traveled on the great circle that connects them).
The class also provides methods latitude and longitude, to access the individual components in a tweet.
• tweet.py contains the Tweet class. An instance of that class represents a single twitter message. The class includes the following methods:
o message() -- returns a string that comprises the full body of the tweet
o position() -- returns a GeoPosition instance describing the location of the tweet.
o timestamp() -- returns a datetime instance describing the day and time at which the tweet was posted. (This information is only relevant for the extra credit challenge.).
• state.py defines a State class used to represent information about a state. Each state has a standard two-letter abbreviation (e.g., MO for Missouri), that is returned by the abbrev() method.
The boundaries of each state are defined with a series of geographic positions. The relevant information about State for you is that the State class supports a method, centroid(), that returns a
single GeoPosition for the centroid of the state. Informally, the centroid is an "average" of all positions in the state, which can be used as an approximation for the entire state for determining the closest state for a tweet.
• us_states.py module contains the actual data needed for representing the United States. You will not need to examine this file; it will be used by other parts of the project.
• country.py defines a Country class that handles the actual rendering of the states. It supports the following two methods: o setFillColor(stateCode, color)
This method causes the state with the given two-letter state code (e.g., 'MO') to be filled with the given color (specified either as a string or an RGB triple). o setTitle(title)
This method sets the title of the window (it is 'United States' by default).
• colors.py provides support for translating the numeric "sentiment" values into an appropriate color based on a fixed gradient suggested by Cynthia Brewer of Penn State University. In particular, the module defines a method:
get_sentiment_color(sentimentValue)
that returns an RGB triple of an appropriate color for the given numeric sentiment value. If None is sent as a parameter, it returns the color gray (which is different than the color indicated by a neutral sentiment of 0.0).
• parse.py includes load_sentiments to load the sentiments dictionary.
• The data folder contains the raw data for sentiment scores and tweets.
• The samples folder contains four examples of complete images for the respective terms: bacon, bieber, cat, and dog. The bieber image is the
one shown at the beginning of this page; others can be viewed for bacon, cat, and dog.
What you need to do
You need to use the data and code provided to generate a sentiment analysis on some topic. All of your code should go in the file trends.py. The file currently has a very basic class definition for a SentimentAnalysis class that loads the sentiments dictionary, the states list, and the Country instance.
Your code needs to read in the data files you are using: there are nine files provided, you can use either the files with the created date or the ones without the created date. You only want to include tweets that have a specified search term, hashtag, or keyword. For example, if you are analyzing the sentiment towards the recent election, you might want to include tweets only if they include Hillary or Trump in the text. You need to write the code to filter the data.
Your primary tasks in this assignment are to loop through the provided data, and for each tweet that you include, compute the average sentiment for that tweet. You can do that by breaking the tweet into a sequence of words and looking up each word in the sentiment dictionary. The sentiment for the tweet is the average of all word sentiments for the tweet.
For example, if the original tweet were
justin bieber...doesn't deserve the award..eminem deserves it.
The words of the tweet should be considered:
['justin', 'bieber', 'doesn', 't', 'deserve', 'the', 'award', 'eminem', 'deserv es', 'it']
Assuming the tweet has a sentiment score (that is, at least one word of the tweet was identified in the sentiments dictionary), assign this tweet's sentiment score to the "closest" state. The rule that you should use is to assign the tweet to whichever state has its centroid closest to the location of the tweet. This is an imperfect rule (for example, because tweets from New York City will actually be closer to the centroid of Connecticut and New Jersey then to the centroid of New York state); but it is an easy rule to implement, and it will do for now.
Once you have scored all tweets and assigned those scores to the appropriate state, compute the cumulative sentiment for each state as the average of all sentiments that were assigned. Then use that sentiment to pick an appropriate color (using the get_sentiment_color function from our colors module), and set the state's color in the visualization.
You should feel free to define any additional functions within the trends.py file that help you organize your code in a more clear and modular fashion.
Command-line arguments
Your program needs to take the search terms as a list, such as
>>python trends.py [‘Trump’, ‘#MakeAmericaGreatAgain’]
if you want to include tweets that match either of the search terms provided. If you only want one search term, you would call your program using
>>python trends.py Hillary
Some options for how you could use this data
• Determine what people are saying in different states, this could include the sentiment only, or the sentiment weighted by the volume of tweets in a state.
• Examine median sentiment values instead of the average sentiment.
• Compare the results of different keywords or hashtags in the results.
• Compare results by region instead of individual states.
Report
Write a short, 1-2 page report describing what you did and any interesting results you generated. Your report should include the following three sections:
Purpose: What is the purpose of the assignment
Procedure: What did you do? What code did you write? What functionality did you implement? What analysis did you do on the data?
Results: What were the results of the project? How did sentiments in different states compare to each other?
Assignment Code
# A fixed gradient of sentiment colors from negative (blue) to positive (red)
# Colors chosen via Cynthia Brewer's Color Brewer (colorbrewer2.com)
SENTIMENT_COLORS = [
(49,54,149), (69,117,180), (116,173,209), (171,217,233),
(224,243,248), (255,255,191), (254,224,144), (253,174,97),
(244,109,67), (215,48,39), (165,0,38), ]
GRAY = (170,170,170)
def get_sentiment_color(sentiment, scale=4):
"""Returns a color corresponding to the sentiment value.
sentiment -- a number between -1 (negative) and +1 (positive)
"""
if sentiment is None:
return GRAY
scaled = (scale * sentiment + 1) / 2.0
index = int( scaled * len(SENTIMENT_COLORS) ) # Rounds down
if index < 0:
index = 0
if index >= len(SENTIMENT_COLORS):
index = len(SENTIMENT_COLORS) - 1
return SENTIMENT_COLORS[index]
Assignment Code
from cs1graphics import Canvas, Text, Polygon, Drawable, Point
class _RenderedState(Drawable):
def __init__(self, state):
Drawable.__init__(self)
self._label = Text(state.abbrev(), 9, Point(*state.centroid().project()))
self._bounds = None
self._polys = []
for k in range(state.numBoundaries()):
b = state.getBoundary(k)
p = Polygon()
for geo in b:
x,y = geo.project()
p.addPoint(Point(x,y))
if x > 0: # hack to deal with Alaska going past 180 date line
self._bounds = _mergeBounds([x,x,y,y], self._bounds)
self._polys.append(p)
def _draw(self):
for p in self._polys:
p._draw()
self._label._draw()
def getBounds(self):
return self._bounds
def setFillColor(self, color):
for p in self._polys:
p.setFillColor(color)
class Country:
def __init__(self, states, width=950):
ratio = 0.5263
self._canvas = Canvas(width, ratio*width)
self._canvas.setTitle('United States')
self._states = {} # map from abbrev to RenderedState
bounds = None
self._canvas.setAutoRefresh(False)
for s in states:
rendered = _RenderedState(s)
self._canvas.add(rendered)
self._states[s.abbrev()] = rendered
bounds = _mergeBounds(rendered.getBounds(), bounds)
self._canvas.zoomView(width/950.0, Point(0,0))
self._canvas.setAutoRefresh(True)
# self._canvas.setView(Point(bounds[0],bounds[3]), Point(bounds[1],bounds[2]))
def setTitle(self, title):
"""Set the Canvas title to the given string."""
self._canvas.setTitle(title)
def setFillColor(self, stateCode, color):
"""Set the fill color of the state with given abbreviation to the indicated color."""
if not isinstance(stateCode, str):
raise TypeError('state code must be a string')
if stateCode not in self._states:
raise ValueError('unknown state code: ' + stateCode)
self._states[stateCode].setFillColor(color)
def createUSA(width=950):
"""Create a Country instance with given width initialized with USA data."""
from state import load_states
states = load_states()
usa = Country(states, width)
return usa
def _mergeBounds(a, b): # assume that one is real
if b is None:
return a
else:
return [min(a[0],b[0]), max(a[1],b[1]), min(a[2],b[2]), max(a[3],b[3])]
if __name__ == '__main__':
import sys
try:
width = int(sys.argv[1])
except:
width = 950
usa = createUSA(width)
Assignment Code
"""cs1graphics.py
...
Go to www.cs1graphics.org for more information.
This is Version 1.2a2 alpha bugfix release (18 January 2012)
Detabified (15 April 2012)
"""
# 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/>.
# Configuration Options
_nativeThreading = False # if True, this allows for true multi-threading
_mathMode = False # if True, coordinate system uses lower-left origin
_RECURSIVE_LIMIT = 10
_debug = 0
_dashMultiplier = 2 # oddity about whether pattern should be (a,b) or (a,b,a,b)
import copy as _copy
import math as _math
import random as _random
import time as _time
import threading as _threading
import atexit as _atexit
import tempfile as _tempfile
import os as _os
import sys as _sys
import traceback as _traceback
from array import array as _array
import cStringIO as _cStringIO
import base64 as _base64
# change in module names for Python 2 vs 3
try:
import Queue as _Queue
except ImportError:
import queue as _Queue # Python 3
try:
import thread as _thread
except ImportError:
import _thread as _thread # Python 3
try:
import Tkinter as _Tkinter
except ImportError:
try:
import tkinter as _Tkinter # Python 3
except ImportError:
raise ImportError('cs1graphics requires that Tkinter be installed')
try:
from PIL import Image as _Image
from PIL import ImageDraw as _ImageDraw
from PIL import ImageTk as _ImageTk
except ImportError:
raise ImportError('cs1graphics requires that PIL be installed')
_pilAvailable = True
# Library
_tkroot = None
_ourRandom = _random.Random()
_ourRandom.seed(1234) # initialize the random seed so that behaviors are reproducible
# support for Python 2.x/3.x.
# We want to use isinstance(foo, basestring) in either case
try:
unicode
except NameError:
basestring = unicode = str
# Global Configuration Controls
def configureNativeThreading():
"""Configures cs1graphics to run in native multi-threaded mode.
By default, the library is predominantly single-threaded, with all
rendering in the primary thread and EventHandlers activated only
when the end of the main thread is reached, or an explicit
(blocking) call to startEventHandling() is made.
On systems that support accessing Tkinter from a secondary thread,
an initial call to this function switches to a multi-threaded
model in which case all rendering is managed by a secondary
thread, and EventHandlers are immediately activated once
registered without blocking the primary thread.
Note: This command must be executed prior to the use of any core
library functionality.
Note: As an alternative, your cs1graphics installation can be
configured to use native threading as the default mode by setting
the variable, _nativeThread = True, in the file cs1graphics.py.
"""
if _graphicsManager._state != 'Initial':
raise GraphicsError('configuration must occur prior to other use of the library')
global _nativeThreading
_nativeThreading = True
def configureMathMode():
"""Forces cs1graphics to use standard math coordinate system.
By default, cs1graphics uses a standard computer graphics
coordinate system with the origin at the top-left and the positive
y-axis oriented downward.
If this function is invoked, it causes canvases to use a standard
mathematics coordinate system with the origin at bottom-left and
the positive y-axis oriented upward. In math mode, a positive
rotation is conventionally counterclockwise rather than clockwise.
NOTE: This command must be executed prior to the use of any core
library functionality.
Note: As an alternative, your cs1graphics installation can be
configured to use the math coordinate system by default by setting
the variable, _mathMode = True, in the file cs1graphics.py.
"""
if _graphicsManager._state != 'Initial':
raise GraphicsError('configuration must occur prior to other use of the library')
global _mathMode
_mathMode = True
def configureSetRecursionLimit(limit):
"""Changes the limit on recursion for drawable inclusion.
In cases such as when adding a layer to itself, the drawing
process is intentionally capped with some maximum recursive depth
to avoid an infinite recursion. By default, that limit is 10.
This function allows that to be changed.
"""
if _graphicsManager._state != 'Initial':
raise GraphicsError('configuration must occur prior to other use of the library')
if not isinstance(limit, int):
raise TypeError('limit should be an integer')
if limit < 1:
raise ValueError('limit must be positive')
global _RECURSIVE_LIMIT
_RECURSIVE_LIMIT = limit
class GraphicsError(Exception):
def __init__(self, message, recoverable=False):
Exception.__init__(self, message)
self._recoverable = recoverable
# Data structures
class _OrderedMap:
"""Implements an ordered map.
Although we do not formally require the keys to be hashable, the
expectation is that they should not be mutated.
By default, ordering is based on < operator, but the user
can provide a non-standard boolean function for comparing keys.
This implementation is based upon an underlying treap.
"""
def _less(a, b):
"""Generic version of comparison function."""
return a < b
_less = staticmethod(_less)
def __init__(self, less=None):
"""Create an empty map.
less is a boolean function with callingsignature less(keyA, keyB)
that returns True if keyA is strictly less than keyB.
If not sent, the default < operator is used.
"""
self._root = None
self._size = 0
if less is not None:
self._less = less
def __len__(self):
"""Return the size of the map."""
return self._size
def _trace(self, key):
"""Walk path looking for given key.
Return the node that has the key, if any.
Otherwise return the last true node visited.
In case of an empty map, None is returned.
"""
if len(self) > 0:
walk = self._root
while walk is not None and
(self._less(key, walk.key) or self._less(walk.key, key)):
# no match thus far
trail = walk
if self._less(key, walk.key):
walk = walk.left
else:
walk = walk.right
if walk is not None:
result = walk
else:
result = trail
else:
result = None
return result
def __delitem__(self, key):
"""Remove the entry assoicated with the key.
KeyError results if key does not exist.
"""
temp = self.find(key)
if temp is None:
raise KeyError(repr(key))
self.remove(temp)
def __getitem__(self, key):
"""Return the value associated with the key.
KeyError results if key does not exist.
"""
temp = self.find(key)
if temp is None:
raise KeyError(repr(key))
else:
return temp.value()
def __setitem__(self, key, value):
"""Associate key to value.
If key exists, old value is overwritten with new.
If key does not exist, it is added to the map.
"""
self.insert(key, value) # ignore return value
def find(self, key):
"""Return an iterator to the key's position, if found.
None is returned if key not found.
"""
walk = self._trace(key)
if walk is not None and not
(self._less(key, walk.key) or self._less(walk.key, key)):
return _OrderedMap.iterator(walk)
else:
return None
def __contains__(self, key):
"""Return True if key in the map."""
return self.find(key) is not None
def first(self):
"""Return iterator to the first element of the map.
None is returned if map is empty.
"""
if len(self) > 0:
return _OrderedMap.iterator(self._root.subtreeMin())
else:
return None
def last(self):
"""Return iterator to the last element of the map.
None is returned if map is empty.
"""
if len(self) > 0:
return _OrderedMap.iterator(self._root.subtreeMax())
else:
return None
def __iter__(self):
"""Return generator for (key,value) pairs."""
walk = self.first()
while walk is not None:
yield (walk.key(), walk.value())
walk = walk.next()
def closestBefore(self, key, strict=True):
"""Return iterator to position at or before the key.
With strict=True (the default), the search looks for an item
that has a key strictly smaller than the given one.
With strict=False, it will return an exact match if possible, and
otherwise the closest before.
Will return None in the case that no earlier key is found.
"""
walk = self._trace(key)
if walk is None:
return None
if self._less(walk.key, key):
# this is strictly smaller than key, so it must be it
return _OrderedMap.iterator(walk)
elif not (strict or self._less(key, walk.key)):
# use the exact match
return _OrderedMap.iterator(walk)
elif walk.left is not None:
# found an exact match, and it has lesser children
return _OrderedMap.iterator(walk.left.subtreeMax())
else:
# start walking upward
while walk is not None and not self._less(walk.key, key):
walk = walk.parent
if walk is not None:
return _OrderedMap.iterator(walk)
else:
return None
def closestAfter(self, key, strict=True):
"""Return iterator to position at or after the key.
With strict=True (the default), the search looks for an item
that has a key strictly larger than the given one.
With strict=False, it will return an exact match if possible, and
otherwise the closest after.
Will return None in the case that no later key is found.
"""
walk = self._trace(key)
if self._less(key, walk.key):
# this is strictly larger than key, so it must be it
return _OrderedMap.iterator(walk)
elif not (strict or self._less(walk.key, key)):
# use the exact match
return _OrderedMap.iterator(walk)
elif walk.right is not None:
# found an exact match, and it has greater children
return _OrderedMap.iterator(walk.right.subtreeMin())
else:
# start walking upward
while walk is not None and not self._less(key, walk.key):
walk = walk.parent
if walk is not None:
return _OrderedMap.iterator(walk)
else:
return None
def insert(self, key, value=None):
"""Associate key to value.
If key exists, old value is overwritten with new.
If key does not exist, it is added to the map.
Return an iterator to the key's position.
"""
walk = self._trace(key)
if walk is None:
self._size += 1
self._root = _OrderedMap._node(key, value)
return _OrderedMap.iterator(self._root)
else:
if self._less(key, walk.key):
walk.left = _OrderedMap._node(key, value, walk)
walk = walk.left
self._insertRebalance(walk)
self._size += 1
elif self._less(walk.key, key):
walk.right = _OrderedMap._node(key, value, walk)
walk = walk.right
self._insertRebalance(walk)
self._size += 1
else:
# key exists; overwrite old value
walk.val = value
return _OrderedMap.iterator(walk)
def _insertRebalance(self, walk):
while walk.parent is not None and walk.priority < walk.parent.priority:
self._rotateUp(walk)
def remove(self, posn):
"""Remove the item at the given iterator."""
if not isinstance(posn, self.iterator):
raise TypeError("Must provide valid iterator for remove")
self._size -= 1
walk = posn._nd
if walk.left is None or walk.right is None:
self._easyDelete(walk)
else:
# use predecessor as sub for the current node
sub = walk.left.subtreeMax()
# fix pointer from above
if self._root is walk:
self._root = sub
elif walk is walk.parent.left:
walk.parent.left = sub
else:
walk.parent.right = sub
# relocate sub and remove walk
if sub is not walk.left:
# clean up below
sub.parent.right = sub.left
if sub.left is not None:
sub.left.parent = sub.parent
# sub takes over left child of walk
sub.left = walk.left
walk.left.parent = sub
# sub takes over right child of walk
sub.right = walk.right
walk.right.parent = sub
# sub gets new parent
sub.parent = walk.parent
# restore heap property from sub downward
downward = True
while downward:
child = sub.left
if sub.right is not None and (child is None or sub.right.priority < child.priority):
child = sub.right
if child is not None and child.priority < sub.priority:
self._rotateUp(child)
else:
downward = False
def _rotateUp(self, walk):
"""Rotate node walk up one level.
Assumes that walk is not the root (but parent may be)
"""
parent = walk.parent
grand = parent.parent
walk.parent = grand
parent.parent = walk
if parent.left is walk:
parent.left = walk.right
if walk.right is not None:
walk.right.parent = parent
walk.right = parent
else:
parent.right = walk.left
if walk.left is not None:
walk.left.parent = parent
walk.left = parent
if grand is None:
self._root = walk
else:
if grand.left is parent:
grand.left = walk
else:
grand.right = walk
def _easyDelete(self, walk):
"""Assumes that walk is a node that has at most one child."""
if walk.left is None:
child = walk.right
else:
child = walk.left
if child is not None:
child.parent = walk.parent
if walk.parent is None:
self._root = child
else:
if walk is walk.parent.left:
walk.parent.left = child
else:
walk.parent.right = child
walk.parent = walk.left = walk.right = None # disconnect, to be safe
###################################################
######### nested class _OrderedMap._node ##########
class _node:
__slots__ = ('key', 'val', 'parent', 'left', 'right', 'priority') # optimization
"""Simple struct to represent node of the treap"""
def __init__(self, key, value=None, parent = None, leftChild = None, rightChild = None):
self.key = key
self.val = value
self.parent = parent
self.left = leftChild
self.right = rightChild
self.priority = _ourRandom.random()
def subtreeMin(self):
"""Return leftmost node of subtree."""
walk = self
while walk.left is not None:
walk = walk.left
return walk
def subtreeMax(self):
"""Return rightmost node of subtree."""
walk = self
while walk.right is not None:
walk = walk.right
return walk
def predecessor(self):
"""Returns node of predecessor. Returns None if this is minimum."""
if self.left is not None:
return self.left.subtreeMax()
else:
walk = self
while walk.parent is not None and walk.parent.left is walk:
walk = walk.parent
return walk.parent
def successor(self):
"""Returns node of successor. Returns None if this is maximum."""
if self.right is not None:
return self.right.subtreeMin()
else:
walk = self
while walk.parent is not None and walk.parent.right is walk:
walk = walk.parent
return walk.parent
######### end of class _OrderedMap._node ##########
######################################################
######### nested class _OrderedMap.iterator ##########
class iterator:
"""Encapsulation of a position in the map"""
def __init__(self, node):
self._nd = node
def __repr__(self):
return "Iterator[key="+repr(self.key())+' value='+repr(self.value())+"]"
def __eq__(self, other):
"""Return True if iterators represent the same position."""
return self._nd == other._nd
def __ne__(self, other):
"""Return True if iterators do not represent the same position."""
return not self._nd == other._nd
def key(self):
"""Return key of element at this position."""
return self._nd.key
def value(self):
"""Return value of element at this position."""
return self._nd.val
def prev(self):
"""Return iterator to the previous element of the map.
Return None if there is no predecessor."""
other = self._nd.predecessor()
if other is not None:
return _OrderedMap.iterator(other)
else:
return None
def next(self):
"""Return iterator to the next element of the map.
Return None if there is no successor."""
other = self._nd.successor()
if other is not None:
return _OrderedMap.iterator(other)
else:
return None
######### end of class _OrderedMap.iterator ##########
class _Hierarchy:
"""Used to maintain minimal information to track which objects are
currently contained (directly or indirectly) on a Canvas, and to
track the parent/child relationships between those objects.
Technically, each object is noted as an (object,cls) pair where
cls is the class whose _draw was called. Typically, this will be
the object's class, but could be a parent class for some.
Furthermore, each object typically has only one such entry in the
hierarchy, but with multiple inheritence (e.g. Button), there
might be three or more different entries, one due to the original
Button._draw call, but two subsequent due to the underlying
Rectangle._draw and Text._draw calls.
"""
def __init__(self):
self._objects = {} # map from obj to set of all (obj,cls) pairs
self._relationships = {} # map from (obj.cls) pair to [parentSet, childrenDict, maxSerial]
# where parentSet is set of (obj,cls) tuples,
# childrenDict is dictionary mapping from (child,cls) -> serialFloat,
# and maxSerial is an upper bound on the serials currently in use
def __contains__(self, drawable):
"""Determines whether the drawable is contained in the current hierarchy."""
return drawable in self._objects
def newCanvas(self, canvas):
"""Adds canvas as new top-level container in the hierarchy."""
self._objects[canvas] = set()
self._objects[canvas].add( (canvas,Canvas) )
self._relationships[ (canvas, Canvas) ] = [set(), {}, 0]
def addLink(self, parentTuple, childTuple):
"""Connect child to parent.
parentTuple and childTuple should both be of form (object,cls)
and that parentTuple is already in this hierarchy.
"""
self._objects.setdefault(childTuple[0], set()).add(childTuple)
relate = self._relationships[parentTuple]
relate[2] += 1 # update serial
relate[1][childTuple] = relate[2] # new child with updated serial
self._relationships.setdefault(childTuple, [set(), {}, 0])[0].add(parentTuple)
def removeLink(self, parentTuple, childTuple):
"""Removes the child from the parent (including the cleansing of any descendents)."""
# remove child from parent's list of children
parentsChildren = self._relationships[parentTuple][1]
del self._relationships[parentTuple][1][childTuple]
# remove parent from child's list of parents
childsParents = self._relationships[childTuple][0]
childsParents.remove(parentTuple)
if not childsParents: # empty set
self._recursiveRemove(childTuple)
def findChildTuple(self, parentTuple, child):
"""For when we know the child, but not the child's appropriate "class" tag
(because _draw was not necessarily from that class)
"""
for k in self._relationships[parentTuple][1].keys():
if k[0] == child:
return k
def getSerial(self, parentTuple, childTuple):
return self._relationships[parentTuple][1][childTuple]
def _recursiveRemove(self, objTuple):
# remove association from self._objects
objSet = self._objects[objTuple[0]]
objSet.remove(objTuple)
if not objSet: # empty set
del self._objects[objTuple[0]]
# remove association from self._relationships
entry = self._relationships.pop(objTuple)
children = entry[1]
for c in children.keys():
childsParents = self._relationships[c][0]
childsParents.remove(objTuple)
if not childsParents: # no more parents
self._recursiveRemove(c)
def reviseChildren(self, drawTuple, childSequence):
"""Compares the newSequence of drawable's children to sequence currently on record.
Returns list of (child,serial) pairs for those children that require updated serial numbers.
"""
raise NotImplementedError('reviseChildren not yet written') # TODO
def computeUpwardChains(self, drawable, counts = None):
if counts is None:
counts = {}
if isinstance(drawable, tuple):
tuples = [ drawable ]
else:
tuples = self._objects[drawable]
results = []
for t in tuples:
self._computeUpwardChainsRecurse(results,t,counts)
if _debug >= 2:
print('ComputeUpwardChains('+str(drawable)+','+str(counts)+') returning:')
for c in results:
print(' '+str(tuple(c)))
return results
def _computeUpwardChainsRecurse(self, results, drawTuple, count):
prevCount = count.get(drawTuple,0)
if prevCount < _RECURSIVE_LIMIT:
parents = self._relationships[drawTuple][0]
if parents:
count[drawTuple] = 1 + prevCount
for p in parents:
oldSize = len(results)
self._computeUpwardChainsRecurse(results, p, count)
for k in range(oldSize, len(results)):
results[k].append(drawTuple)
count[drawTuple] -= 1 # decrement count, to avoid side effects
if count[drawTuple] == 0:
del count[drawTuple]
else:
results.append( [drawTuple] ) # "drawTuple" must represent a canvas
def computeDownwardChains(self, drawTuple):
"""Computes all downward chians from the given starting point.
Returns pre-order list of (chain, countDict) pairs
Allows for cycles in chain, up to the globally determined recursive limit.
"""
results = []
self._computeDownwardChainsRecurse(results, drawTuple, {})
if _debug >= 2:
print('ComputeDownwardChains('+str(drawTuple)+') returning:')
for c in results:
print(' '+str(tuple(c)))
return results
def _computeDownwardChainsRecurse(self, results, drawTuple, count):
"""
Returns a pre-order list of all downward chains (including all prefixes).
Furthermore this version is given a dictionary of counts, mapping from
drawTuple -> frequency that is presumed to have occurred
outside the context of this call (zero if not present).
Semantic is that there is a total cap on the number of
occurrences of any given element, including the previous
counts.
Note: this function must guarantee that count is restored to
its previous state by the end of a given call so that there
are no lasting side effect (except perhaps by having non-keys
end up as keys with a count of zero).
"""
prevCount = count.get(drawTuple, 0)
count[drawTuple] = 1 + prevCount
results.append( ([drawTuple], dict(count)) )
for child in self._relationships[drawTuple][1].keys():
if count.get(child, 0) < _RECURSIVE_LIMIT:
oldSize = len(results)
self._computeDownwardChainsRecurse(results, child, count)
for k in range(oldSize, len(results)):
results[k][0].insert(0, drawTuple)
count[drawTuple] -= 1 # decrement count to avoid lasting effect
if count[drawTuple] == 0:
del count[drawTuple]
class _RenderedHierarchy:
class Node:
__slots__ = ('_chain', '_children', '_sortedChildren', '_prev', '_next', '_parent', # optimization
'_depth', '_transformation', '_cumulativeTransformation', '_renderedDrawable')
def __init__(self):
self._chain = None
self._children = dict()
self._sortedChildren = _OrderedMap()
self._prev = None
self._next = None
self._parent = None
self._depth = None
self._transformation = _Transformation()
self._cumulativeTransformation = _Transformation()
self._renderedDrawable = None
def __init__(self):
self._root = self.Node()
self._first = None
self._last = None
self._nodeLookup = dict()
self._nodeLookup[tuple()] = self._root
def add(self, chain, depth, transformation, renderedDrawable):
"""Add a new chain to the hierarchy and return the new node.
The parent chain must be present.
"""
parentChain = chain[:-1]
parentNode = self._nodeLookup[parentChain]
# Create the new node
newNode = self.Node()
newNode._chain = chain
newNode._depth = depth
newNode._transformation = transformation
newNode._cumulativeTransformation = parentNode._cumulativeTransformation*transformation
newNode._renderedDrawable = renderedDrawable
newNode._parent = parentNode
# Link new node into structure
self._nodeLookup[chain] = newNode
parentNode._children[chain[-1]] = newNode
parentNode._sortedChildren[depth] = newNode
self._addThreads(newNode, parentNode)
return newNode
def remove(self, chain):
"""Remove a node and all of its children.
A list of RenderedDrawables to be deleted is returned.
"""
node = self._nodeLookup[chain]
parentChain = chain[:-1]
parentNode = self._nodeLookup[parentChain]
# Remove parent references and threads
parentNode._children.pop(chain[-1])
del parentNode._sortedChildren[node._depth]
self._removeThreads(node, parentNode)
# Find all of the RenderedDrawables to delete
deleted = list()
queue = [node]
while len(queue) > 0:
n = queue.pop()
self._nodeLookup.pop(n._chain)
if n._renderedDrawable is not None:
deleted.append(n._renderedDrawable)
queue.extend(n._children.values())
return deleted
def prev(self, node):
"""Find the previous leaf node.
Precondition: node is a leaf node
If there is no previous node it returns None
"""
return node._prev
def next(self, node):
"""Find the next leaf node.
Precondition: node is a leaf node
If there is no next node it returns None
"""
return node._next
def first(self, node):
while len(node._sortedChildren) > 0:
node = node._sortedChildren.first().value()
return node
def last(self, node):
while len(node._sortedChildren) > 0:
node = node._sortedChildren.last().value()
return node
def getNode(self, chain):
return self._nodeLookup[chain]
def hasChain(self, chain):
return chain in self._nodeLookup
def getDepth(self, chain):
return self._nodeLookup[chain]._depth
def changeDepth(self, chain, newDepth):
node = self._nodeLookup[chain]
oldDepth = node._depth
if _debug >= 1.5: print('change depth of ' + str(chain) + ' from ' + str(oldDepth) + ' to ' + str(newDepth))
node._depth = newDepth
parent = node._parent
handle = parent._sortedChildren.find(oldDepth)
prevSib = handle.prev()
nextSib = handle.next()
del parent._sortedChildren[oldDepth]
parent._sortedChildren[newDepth] = node
if (prevSib is not None and newDepth < prevSib.key()) or
(nextSib is not None and newDepth > nextSib.key()):
# must re-thread relative to siblings
if _debug >= 2.5:
for (k,v) in iter(parent._sortedChildren):
print( ' child: ' + str(k) + ' ' + str(v))
self._removeThreads(node, parent) # detach from old location
self._addThreads(node, parent) # reattach in new location
return (self.first(node), self.last(node)) # Return range of things that need to be changed
else:
return (None, None)
def changeTransform(self, chain, newTransform):
node = self._nodeLookup[chain]
node._transformation = newTransform
node._cumulativeTransformation = node._parent._cumulativeTransformation * newTransform
# Propogate to children
toFix = list(node._children.values())
while len(toFix) > 0:
n = toFix.pop()
n._cumulativeTransformation = n._parent._cumulativeTransformation * n._transformation
toFix.extend(n._children.values())
return (self.first(node), self.last(node)) # Return range of things that need to be changed
def _addThreads(self, newNode, parentNode):
"""Adjust the threads to incorporate a recently added node/subtree."""
# Find extremes in current threading (might be subtree)
first = self.first(newNode)
last = self.last(newNode)
# establish links from new tree to rest
if len(parentNode._children) == 1:
first._prev = parentNode._prev
last._next = parentNode._next
parentNode._prev = None
parentNode._next = None
else:
p = parentNode
c = newNode
while p is not None and p._sortedChildren.first().value() == c:
c = p
p = p._parent
if p is None:
first._prev = None
last._next = self._first
else:
neighbor = p._sortedChildren.find(c._depth)
first._prev = self.last(p._sortedChildren.find(c._depth).prev().value())
last._next = first._prev._next
# establish links from rest back to new tree
if first._prev is None:
self._first = first
else:
first._prev._next = first
if last._next is None:
self._last = last
else:
last._next._prev = last
def _removeThreads(self, node, parentNode):
"""Adjust the threads to disengage a node/subtree that is being moved/removed."""
# Fix threading
if len(parentNode._children) == 0: # Parent is now a leaf
parentNode._prev = self.first(node)._prev
parentNode._next = self.last(node)._next
if parentNode._prev is None:
self._first = parentNode
else:
parentNode._prev._next = parentNode
if parentNode._next is None:
self._last = parentNode
else:
parentNode._next._prev = parentNode
else:
first = self.first(node)
last = self.last(node)
if first._prev is None:
self._first = last._next
else:
first._prev._next = last._next
if last._next is None:
self._last = first._prev
else:
last._next._prev = first._prev
class _UpdateManager:
"""This is a structure to manage pending updates until they are
ready to be passed on to the _RenderedManager.
Internally, it is modeled upon the underlying hierarchy, but
compressed so that it only has nodes for those elements with a
pending update. This means that siblings are guaranteed to be
prefix-free of each other, although they may share a commond prefix.
"""
#------------------- inner _node class -----------------
class _node:
"""A basic inner class for a node in the tree.
status will be maintained either as 'stable', 'remove', or 'add'
Frozen nodes will need to mantain two states. A "private" view
that is the state of the object as it would appear if
subsequently unfrozen. The "public" state is a representation
of the state of the object at the time that it was most
recently frozen (and thus how it should currently be rendered,
if needed). That public view is modeled as if its entire
subtree is unfrozen (even if those nodes have corresponding
private nodes that are truly frozen).
Unfrozen nodes only have a public view, which can be a mix of
frozen and unfrozen nodes as needed.
"""
__slots__ = ('_chain', '_publicChildren', '_privateChildren', '_publicUpdates',
'_privateUpdates', '_status', '_special') # optimization
def __init__(self, chain):
"""New node is presume 'stable' unless informed subsequently"""
self._chain = chain
self._publicChildren = _OrderedMap()
self._privateChildren = None
self._publicUpdates = {}
self._privateUpdates = None
self._status = 'stable' # the default
self._special = '' # used for special cases with propogating private/public branches
def isFrozen(self):
"""Is this node representing a directly frozen element.
Note: to be distinguished from indirect freeze of an ancestor
"""
return self._privateUpdates is not None
def doFreeze(self):
"""Freeze a node
_privateUpdates becomes empty dictionary.
existing (public) children must be splintered into
appropriate private/public components.
"""
if self._privateUpdates is None: # i.e., not currently frozen
self._privateUpdates = {}
self._privateChildren = _OrderedMap()
def doUnfreeze(self):
"""For new unfreeze, everything in private is pushed to public."""
# doing this first step before checking frozen, because a mirrored subtree
# might not look frozen, even though its mirror is. Need to note that so
# that unfreeze is propogated later.
if self._special != 'remove':
self._special = 'unfreeze' # remove trumps unfreeze
if self.isFrozen():
self._publicUpdates.update(self._privateUpdates)
self._privateUpdates = None
# any private updates must be converted to public
rest = self._privateChildren
self._privateChildren = None # hide this before re-inserting updates
self._resolveMirror(rest)
def _resolveMirror(self, privateMap):
"""Send updates to public branch that were buffered in private mirror."""
# the key is that anything that happened in private branch
# must have happened subsequent to the time that a mirror
# was originally created. When getting rid of the mirror,
# we must carefully propogate a set of updates back to the
# public branch to reflect the sequence of events.
#
# Special care is needed in the case that unfreezes
# occurred or that remove/add pairs took place, since
# those events should cause changes to the state of the
# public branch.
if _debug >= 2:
print("Within _resolveMirror on node " + str(self))
for (chain, child) in list(privateMap):
if _debug >= 3:
print("Resolving child " + str(child) + " with status " + child._status + " and special " + child._special)
if child._special == 'remove': # anything else here was after the remove
self._updateRecurse(chain, 'remove', {}, privateMap)
if child._status == 'stable': # must have been re-added subsequently
self._updateRecurse(chain, 'add')
elif child._special == 'unfreeze': # must propogate
self._updateRecurse(chain, 'unfreeze')
if child._status == 'add':
self._updateRecurse(chain, 'add', child._publicUpdates)
elif child._publicUpdates:
self._updateRecurse(chain, 'update', child._publicUpdates)
if child._publicChildren:
self._resolveMirror(child._publicChildren) # recurse, with updates sent to this node
if child.isFrozen():
self._updateRecurse(chain, 'freeze')
if child._privateUpdates:
self._updateRecurse(chain, 'update', child._privateUpdates)
if child._privateChildren:
self._resolveMirror(child._privateChildren) # recurse, with updates sent to this node
def setProperties(self, properties):
"""Properties can be any dictionary of kev/value pairs.
This assumes that frozen status is current"""
if self.isFrozen():
self._privateUpdates.update(properties)
else:
self._publicUpdates.update(properties)
def setBorn(self):
"""Schedule an element as newly born.
We presume that frozen status was already set before this call.
"""
if self._status == 'remove':
self._status = 'stable' # rendered already existed
else:
self._status = 'add'
def setDead(self, parentMap):
"""Schedule an element to die.
If it has not previously been rendered, then the node is
deleted entirely as it becomes irrelevant.
If it was previously rendered, it is scheduled to die, but
all other pending updates are flushed since they become
irrelevant.
parentMap should be the _children map containing this node
as a value.
"""
if self._status == 'add':
# we can go ahead and kill this right away, as well as all descendents
# (which by definition should be new)
del parentMap[self._chain]
else:
# cannot kill yet, since rendering already exists.
# But we can clear all pending properties/updates and
# effectively unfreze.
self._status = self._special = 'remove' # note well that we set _special as well
self._publicUpdates = {}
self._publicChildren = _OrderedMap()
self._privateUpdates = None
self._privateChildren = None
def _updateRecurse(self, chain, style, properties={}, parentMap=None):
"""Note that parentMap need only be sent when style is 'remove'."""
if _debug >= 3:
print('_UpdateManager._node._updateRecurse called with
' + '
'.join([str(x) for x in (self,chain,style,properties)]))
print(' Node is currently ' + ('frozen' if self.isFrozen() else 'unfrozen'))
if self._chain == chain:
# exact match; make the changes
if style == 'remove':
self.setDead(parentMap)
elif style == 'freeze':
self.doFreeze()
elif style == 'unfreeze':
self.doUnfreeze()
else:
# either 'update' or 'add'
if style == 'add':
self.setBorn()
self.setProperties(properties)
else:
# figure out how to recurse; all updates go to private branch if frozen
if self.isFrozen():
children = self._privateChildren
else:
children = self._publicChildren
before = children.closestBefore(chain, False)
if before is not None:
val = before.key()
if chain[:len(val)] == val: # prefix or exact match
before.value()._updateRecurse(chain, style, properties, children)
return
else:
after = before.next()
else:
after = children.first()
# if we reach this point, we need to make a new child,
# check for other children that should be contained under
# new child, then recurse (on what will be base case)
child = _UpdateManager._node(chain)
if _debug >= 2.5:
print("created new _UpdateManager._node: " + str(child) + " for chain " + str(chain))
while after is not None and after.key()[:len(chain)] == chain:
relocate = after
after = relocate.next()
child._publicChildren[relocate.key()] = relocate.value() # reinsert under new node (always public)
children.remove(relocate) # and remove from current level
children[chain] = child # add child
child._updateRecurse(chain, style, properties, children)
def _flushRecurse(self, parentMap=None):
if _debug >= 3:
print('_flushRecurse called on node ' + str(self))
print('isFrozen currently' + str(self.isFrozen()))
if self._status != 'stable' or len(self._publicUpdates) > 0:
# this node needs to be added/removed or has properties to push
yield (self._chain, self._status, self._publicUpdates)
self._publicUpdates = {}
self._status = 'stable'
# consider all public children, even if current node is frozen
for (key,c) in list(self._publicChildren): # use copy, since calls may mutate
for result in c._flushRecurse(self._publicChildren):
yield result
if parentMap is not None and not self.isFrozen():
# this node has no private data, and all public updates will have been pushed
# only issue is remaining (frozen) children. Let's destroy this node, and promote
# any remaining children in its place.
for (_,c) in self._publicChildren:
parentMap[c._chain] = c # move this node's remaining children to parent
del parentMap[self._chain] # and then remove this node, since flushed
#------------------- end of inner _node class -----------------
def __init__(self):
"""An initially empty Hierarchy.
Initialized to have a persistent root with () chain.
"""
self._root = self._node(())
def update(self, chain, style, properties={}):
"""Augment the manager with the given update.
style should be either 'add', 'remove', 'freeze', 'unfreeze', or 'update'
For 'add' or 'update', properties should be dictionary of key/value pairs.
Empty dicitonary should be used for remove/freeze/unfreeze.
"""
if _debug >= 1:
print('
_UpdateManager.update called with
' + '
'.join([str(x) for x in (chain,style,properties)]))
if not isinstance(style, basestring):
raise TypeError('style should be a string')
if style not in ('add', 'remove', 'freeze', 'unfreeze', 'update'):
raise ValueError('invalud style designator: ' + str(statusFlags))
if not isinstance(properties, dict):
raise TypeError('properties should be a dictionary')
if style in ('remove', 'freeze', 'unfreeze') and len(properties) > 0:
raise ValueError('Must send empty dictionary with ' + style)
self._root._updateRecurse(chain, style, properties, self._root._publicChildren)
def flush(self):
"""This returns a preorder generator of all updates to be rendered.
In the process, it mutates the UpdateManager to remove nodes
associated with objects that will presumably be deleted from
the rendering.
Objects yielded are (chain, status, properties)
where status is 'add', 'remove', or 'stable' and properties is a dictionary
"""
if _debug >= 1:
print('_UpdateManager.flush() called')
return self._root._flushRecurse()
class _GraphicsManager:
def __init__(self):
# Synchornization mechanisms
self._state = 'Initial' # 'Initial', 'Running', 'Stopped' or 'Failed'
self._commandQueue = _Queue.Queue()
self._commandLock = _threading.RLock() # Must be grabbed before working with command queue
self._resultQueue = _Queue.Queue()
self._functionLock = _threading.RLock()
# Rendering engine objects
# _frontHierarchy manages the view based on what has been sent to the command queue
self._frontHierarchy = _Hierarchy()
# _middleHierarchy is based on middle layer that has pulled
# stuff off the command queue and sent to update manager.
# When commandQueue is empty, this should match _frontHierarchy
self._middleHierarchy = _Hierarchy()
# _middleProperties is used to cache all of the drawable
# properties for those objects known to the middle layer.
# These are needed for times when the middle layer must
# retransmit them to the update manager when adding a
# secondary chain for existing objects
self._middleProperties = {} # map from Drawable -> dictionary of properties
# _updateMangager lies between the middle and back layer. It
# handles batching changes as well as the semantics for
# freezing canvases or drawables
self._updateManager = _UpdateManager()
# _renderedHierarchy structure is based on what has already
# been flushed through the update manager, and hence what is
# currently rendered at the Tk level
self._renderedHierarchy = _RenderedHierarchy()
# Status
self._openCanvases = []
self._drawParent = None
self._drawChildren = None
# Event handling
# _handlingEvents could be Always, Yes, No or Waiting
if _nativeThreading:
self._handlingEvents = 'Always'
else:
self._handlingEvents = 'No'
self._waitingObject = None
self._eventQueue = _Queue.Queue()
self._eventHandlers = dict()
self._objectIdRegistry = dict()
self._lastEvent = None
self._eventLock = _threading.RLock() # TODO lock every event thing up
# Mouse
self._mousePrevPosition = None
self._mouseButtonDown = False
def beginRefresh(self):
self._commandLock.acquire()
def completeRefresh(self, pushUpdates=True):
# TODO: in single-threaded, wait until LAST reentrant lock released before pushing
if pushUpdates:
self.addCommandToQueue(('push updates',))
if not _nativeThreading:
self.processCommands()
_tkroot.update()
self._commandLock.release()
def addCommandToQueue(self, command):
if self._state == 'Initial':
self._state = 'Running'
if _nativeThreading:
# Start command thread
_thread.start_new_thread(_startCommandThread, ())
_atexit.register(_stopCommandThread)
else:
_initLibrary()
_atexit.register(_exitMainThread)
if self._state != 'Failed':
if _debug >= 1:
print('addCommandToQueue: ' + str(command))
self._commandQueue.put(command)
def _closeAll(self):
pass # TODO
def processCommands(self):
MAX_TIME = 0.1
start_time = _time.time()
try:
while (_time.time() - start_time) <= MAX_TIME and self._state == 'Running' and not self._commandQueue.empty():
command = self._commandQueue.get(False)
try:
self.processCommand(command)
except GraphicsError:
raise
except KeyboardInterrupt:
raise
except GraphicsError:
raise
except: # TODO: too general?
# Note: could happen for an empty queue
print('Unknown graphics error has occured. Graphics manager is shutting down.')
print('Program must be restarted to use graphics.')
print('If problem is repeatable, please report to ....')
self._state = 'Failed'
self._closeAll()
if _debug > 0: # exit upon first error
_traceback.print_exc(file=_sys.stdout)
_sys.exit()
def serializeDepth(self, original, parentTuple, leafTuple):
serial = -self._middleHierarchy.getSerial(parentTuple, leafTuple) # negated to get painter's ordering
if isinstance(parentTuple[0], (Canvas,Layer)):
depthKey = (original, serial)
else: # user-defined
depthKey = (None, serial)
return depthKey
def processCommand(self, command):
if _debug >= 1:
print('')
print('Manager executing: ' + str(command))
# Rendering
if command[0] == 'push updates':
self._pushUpdates()
# Canvases
elif command[0] == 'create canvas':
chain = ((command[1],Canvas),)
self._updateManager.update(chain, 'add', command[2])
self._middleHierarchy.newCanvas(command[1])
if command[2]['frozen']:
self._updateManager.update(chain, 'freeze')
elif command[0] == 'close canvas':
_tkroot.update()
# existing object is frozen
elif command[0] == 'freeze':
for chain in self._middleHierarchy.computeUpwardChains(command[1]):
self._updateManager.update(tuple(chain), 'freeze')
# existing object is unfrozen
elif command[0] == 'unfreeze':
for chain in self._middleHierarchy.computeUpwardChains(command[1]):
self._updateManager.update(tuple(chain), 'unfreeze')
# New objects
elif command[0] == 'object added':
containerTuple = command[1]
drawableTuple = command[2]
if _debug >= 1:
print('_middleHierarchy.addLink: ' + str(containerTuple) + ' ' + str(drawableTuple))
self._middleHierarchy.addLink(containerTuple, drawableTuple)
downwardChains = self._middleHierarchy.computeDownwardChains(drawableTuple)
for d,count in downwardChains:
leafTuple = d[-1]
properties = dict(self._middleProperties[leafTuple[0]]) # intentional copy
isFrozen = properties['frozen']
if len(d) > 1:
# we know the parent for all such chains
parentTuple = d[-2]
else:
parentTuple = containerTuple
properties['depth'] = self.serializeDepth(properties['depth'], parentTuple, leafTuple)
for u in self._middleHierarchy.computeUpwardChains(containerTuple, count):
tc = tuple(u + d)
if _debug >= 1.5:
print('
Adding chain to updateManager: ' + repr(tc))
print("Effective depth " + str(properties['depth']))
self._updateManager.update(tc, 'add', properties)
if isFrozen:
self._updateManager(tc, 'freeze')
elif command[0] == 'object removed':
parent = command[1]
child = command[2]
for c in self._middleHierarchy.computeUpwardChains(parent):
c.append( child )
self._updateManager.update(tuple(c), 'remove')
if _debug >= 1:
print('_middleHierarchy.removeLink: ' + str(parent) + ' ' + str(child))
self._middleHierarchy.removeLink(parent,child)
# Drawables
elif command[0] == 'update':
self._middleProperties.setdefault(command[1],{}).update(command[2])
if command[1] in self._middleHierarchy:
for chain in self._middleHierarchy.computeUpwardChains(command[1]):
if 'depth' in command[2]:
parentTuple = chain[-2]
childTuple = chain[-1]
command[2]['depth'] = self.serializeDepth(command[2]['depth'], parentTuple, childTuple)
if _debug >= 1:
print('Updating Effective Depth: %s' % str(command[2]['depth']))
self._updateManager.update(tuple(chain), 'update', command[2])
elif command[0] == 'load image':
s = command[1]
good = True
i = None
try:
if s[:7] != "base64:":
i = _Tkinter.PhotoImage(file=command[1])
else:
data = _base64.b64decode(s[7:])
r = _cStringIO.StringIO(data)
i = _ImageTk.PhotoImage(_Image.open(r).convert('RGBA'))
except:
good = False
if good:
self._resultQueue.put( (i, i.width(), i.height()) )
else:
self._resultQueue.put(None)
elif command[0] == 'convert image':
self._resultQueue.put(_convertImage(command[1]))
elif command[0] == 'get text size':
self._resultQueue.put(_getTextSize(command[1], command[2]))
elif command[0] == 'save to file':
rc = self._renderedHierarchy.getNode( ((command[1],Canvas),) )._renderedDrawable
rc.saveToFile(command[2], command[3])
self._resultQueue.put(None)
def _pushUpdates(self):
# Loop through update manager, adjust the rendered hierarchy and rendering
if _debug >= 1:
print("_pushUpdates called")
for (chain, status, properties) in self._updateManager.flush():
if _debug >= 1:
print('_pushUpdates: ' + str(status)+' '+str(chain)+' '+str(properties))
if self._renderedHierarchy.hasChain(chain):
print(' Rendered Depth is ' + str(self._renderedHierarchy.getNode(chain)._depth))
if status == 'add':
assert not self._renderedHierarchy.hasChain(chain)
current = self._renderedHierarchy.add(chain, properties['depth'], properties['transformation'], None)
current._renderedDrawable = self._createRendered(chain, properties)
if not isinstance(current._renderedDrawable, _RenderedCanvas) and current._renderedDrawable is not None:
node = current._next
while node is not None and node._renderedDrawable is None:
node = node._next
if node is not None and node._chain[0] == chain[0]: # TODO: correct treatment of forest???
if _debug >= 1: print('Putting '+str(current._renderedDrawable)+' above '+str(node._renderedDrawable))
current._renderedDrawable.putAbove(node._renderedDrawable)
else:
if _debug >= 1: print('Putting '+str(current._renderedDrawable)+' at bottom')
current._renderedDrawable.putAbove(None)
elif status == 'remove':
removed = self._renderedHierarchy.remove(chain)
for renderedDrawable in removed:
renderedDrawable.remove()
elif status == 'stable':
# Update transformation
if 'transformation' in properties:
(first, last) = self._renderedHierarchy.changeTransform(chain, properties['transformation'])
current = first
while True:
if current._renderedDrawable is not None:
current._renderedDrawable.update({'transformation': current._transformation})
if current == last:
break
current = current._next
del properties['transformation'] # will not need to update this below
# Update depth
if 'depth' in properties:
(first, last) = self._renderedHierarchy.changeDepth(chain, properties['depth'])
if first is not None: # something changed
if _debug >= 1.5: print('first, last = '+str( (first._renderedDrawable,last._renderedDrawable) ))
# first goal is finding an adequate anchor below this group
below = last._next
while below is not None and below._renderedDrawable is None:
below = below._next
# now place series of objects in line after each other
current = last
while current != first._prev:
if current._renderedDrawable is not None:
if below is not None:
if _debug >= 1.5:
print('Putting '+str(current._renderedDrawable)+' above '+str(below._renderedDrawable))
current._renderedDrawable.putAbove(below._renderedDrawable)
else:
if _debug >= 1.5:
print('Putting ' + str(current._renderedDrawable) + ' at bottom')
current._renderedDrawable.putAbove(None)
below = current
current = current._prev
# Update any other properties (beyond transformation, depth)
if properties:
rd = self._renderedHierarchy.getNode(chain)._renderedDrawable
if rd is not None:
rd.update(properties)
def _createRendered(self, chain, properties):
subchain = chain[-1][0]
if isinstance(subchain, Canvas):
return _RenderedCanvas(chain, properties)
elif isinstance(subchain, Circle):
return _RenderedCircle(chain, properties)
elif isinstance(subchain, Ellipse):
return _RenderedCircle(chain, properties) # note well: using _renderedCircle
elif isinstance(subchain, Rectangle): # note well: Square qualifies
return _RenderedRectangle(chain, properties)
# For next pair of cases, we test for more specific Polygon before Path.
# Also, note that we render ClosedSpline as a Polygon and Spline as a Path
elif isinstance(subchain, Polygon):
return _RenderedPolygon(chain, properties)
elif isinstance(subchain, Path):
return _RenderedPath(chain, properties)
elif isinstance(subchain, Text):
return _RenderedText(chain, properties)
elif isinstance(subchain, Image):
return _RenderedImage(chain, properties)
def executeFunction(self, command):
# Perform a single command and return a value
# TODO: avoid possible deadlock
self._functionLock.acquire()
self._commandLock.acquire()
self.addCommandToQueue(command)
if not _nativeThreading:
self.processCommands()
_tkroot.update()
self._commandLock.release()
result = self._resultQueue.get()
self._functionLock.release()
return result
def addEventToQueue(self, handler, event):
if self._handlingEvents == 'Always':
# Start a new thread and go
pass # TODO
elif self._handlingEvents == 'Yes':
self._eventQueue.put((handler,event))
elif self._handlingEvents == 'Waiting' and event._trigger == self._waitingObject:
self._eventQueue.put((handler,event))
else:
pass # Ignore the event
def addHandler(self, obj, handler):
#handlers = self._eventHandlers.get(obj, set())
#handlers.add(handler)
#self._eventHandlers[obj] = handlers
self._eventHandlers.setdefault(obj, set()).add(handler)
def removeHandler(self, obj, handler):
s = self._eventHandlers.get(obj, set())
if handler in s:
s.remove(handler) #
else:
raise ValueError()
def processEvents(self):
while not self._eventQueue.empty():
(handler, event) = self._eventQueue.get(False)
self._lastEvent = event
if self._handlingEvents == 'Waiting':
self._handlingEvents = 'No'
while not self._eventQueue.empty():
self._eventQueue.get(False)
handler.handle(event)
def wait(self, waiter):
if self._handlingEvents == 'Always':
lock = _threading.Lock()
rh = _ReleaseHandler(lock)
return rh._event
elif self._handlingEvents == 'No':
self.addHandler(waiter, EventHandler())
self.mainLoop(waiter, True)
return self._lastEvent
def mainLoop(self, waiting=None, exitOnAllClosed=True):
if waiting:
self._handlingEvents = 'Waiting'
self._waitingObject = waiting
while self._state == 'Running' and self._handlingEvents in ('Yes', 'Waiting'):
_tkroot.update()
self.processEvents()
if exitOnAllClosed and len(_graphicsManager._openCanvases) == 0:
break
_time.sleep(.1)
# Events Primatives
class Event(object):
"""An event typically triggered by the user interface."""
def __init__(self):
self._eventType = ''
self._x, self._y = 0, 0
self._prevx, self._prevy = 0,0
self._key = ''
self._button = None
self._trigger = None
def getDescription(self):
"""Return a text description of the event.
Possibilities include:
'mouse click', 'mouse release', 'mouse drag', 'keyboard, 'timer', 'canvas close'
"""
return self._eventType
def getMouseLocation(self):
"""Return a Point designating the location of the mouse at the time of the event."""
return Point(self._x, self._y)
def getOldMouseLocation(self):
"""Return a Point designating the location of the mouse at the start of a mouse drag."""
return Point(self._prevx, self._prevy)
def getTrigger(self):
"""Return a reference to the object that triggered the event."""
return self._trigger
def getKey(self):
"""Return a string designating the key pressed for a keyboard event."""
return self._key
def getButton(self):
"""Return number of the mouse button that caused the mouse event (else None)."""
return self._button
class EventHandler(object):
"""A base class for creating new event handlers.
The handle method for this base class does not do anything.
"""
def __init__(self):
"""Create a new event handler.
Children of this class must call this constructor.
"""
pass
def handle(self, event):
"""Handle an event.
Child classes must override this method, but do not need
to call it.
"""
pass
class _ReleaseHandler(EventHandler):
def __init__(self, lock):
self._lock = lock
self._event = None
self._lock.acquire()
def handle(self, event):
if event.getDescription() in ['keyboard', 'mouse click', 'canvas close']:
self._event = event
self._lock.release()
class _EventTrigger(object):
def __init__(self):
pass
def wait(self):
"""Wait for an event to occur.
When an event occurs, an Event instance is returned
with information about what has happened.
"""
return _graphicsManager.wait(self)
def addHandler(self, handler):
"""Register an EventHandler instance with this object."""
if not isinstance(handler, EventHandler):
raise TypeError('Only instance of EventHandler (or child class) can handle events')
try:
_graphicsManager.addHandler(self, handler) # TODO should be a on queue, not thread safe
except ValueError:
raise ValueError('Handler is already handling events for this object')
def removeHandler(self, handler):
"""Unregister an EventHandler instance from this object."""
if not isinstance(handler, EventHandler):
raise TypeError('Parameter is not an instance of EventHandler (or child class)')
try:
_graphicsManager.removeHandler(self, handler) # TODO should be a on queue, not thread safe
except ValueError:
raise ValueError('The handler is not currently associated with this object.')
class _EventThread(_threading.Thread):
def __init__(self, handler, event):
_threading.Thread.__init__(self)
self._handler = handler
self._event = event
def run(self):
self._handler.handle(self._event)
# Graphics Primatives
class Point(object):
"""Stores a two-dimensional point using Cartesian coordinates."""
def __init__(self, initialX=0, initialY=0):
"""Create a new point instance.
initialX x-coordinate of the point (default 0)
initialY y-coordinate of the point (default 0)
"""
if not isinstance(initialX, (int, float)):
raise TypeError('x-coordinate must be a number')
if not isinstance(initialY, (int, float)):
raise TypeError('y-coordinate must be a number')
self._x = initialX
self._y = initialY
def getX(self):
"""Return the x-coordinate."""
return self._x
def setX(self, val):
"""Set the x-coordinate to val."""
if not isinstance(val, (int, float)):
raise TypeError('x-coordinate must be a number')
self._x = val
def getY(self):
"""Return the y-coordinate."""
return self._y
def setY(self, val):
"""Set the y-coordinate to val."""
if not isinstance(val, (int, float)):
raise TypeError('y-coordinate must be a number')
self._y = val
def get(self):
"""Return an (x,y) tuple."""
return self._x, self._y
def scale(self, factor):
"""Scale the coordinates by the given factor."""
if not isinstance(factor, (int, float)):
raise TypeError('scaling factor must be a number')
self._x *= factor
self._y *= factor
def distance(self, other):
"""Return the distance between this point and the other."""
if not isinstance(other, Point):
raise TypeError('other must be a Point instance')
dx = self._x - other._x
dy = self._y - other._y
return _math.sqrt(dx * dx + dy * dy)
def normalize(self):
"""Mutate the point, scaling it to distance one from the origin.
If the point currently represents the origin, it is unchanged.
"""
mag = self.distance( Point() )
if mag > 0:
self.scale(1./mag)
def __str__(self):
"""Return a string representation of the point (e.g., '<0,0>')."""
return '<' + str(self._x) + ',' + str(self._y) + '>'
def __neg__(self):
"""Return a new point that is the negated version of this Point."""
return Point(-self._x, -self._y)
def __add__(self, other):
"""Return a new point that is the sum of this Point and the other."""
if not isinstance(other, Point):
raise TypeError('both operands must be Point instances')
return Point(self._x + other._x, self._y + other._y)
def __sub__(self, other):
"""Return a new point that is the oriented difference between the points."""
if not isinstance(other, Point):
raise TypeError('both operands must be Point instances')
return Point(self._x - other._x, self._y - other._y)
def __mul__(self, operand):
"""Return the result when multiplying the Point by an operand.
When the operand is a scalar (i.e., an int or float), return a
Point that has coordinates equal to the original times the factor.
When operand is another Point, return a scalar that is the dot
product of the two points.
"""
if isinstance(operand, (int, float)): # multiply by constant
return Point(self._x * operand, self._y * operand)
elif isinstance(operand, Point): # dot-product
return self._x * operand._x + self._y * operand._y
else:
raise TypeError('unexpected operand for multiplication')
def __rmul__(self, operand):
"""Return the result when multiplying the Point by an operand.
See __mul__ for details.
"""
return self * operand
def __xor__(self, angle):
"""Return a point instance equal to a rotated version of the original.
angle number of degrees of rotation
Rotation is performed about the origin.
"""
if not isinstance(angle, (int, float)):
raise TypeError('angle must be a number')
angle = _math.pi*angle/180.
return Point(self._x * _math.cos(angle) - self._y * _math.sin(angle),
self._x * _math.sin(angle) + self._y * _math.cos(angle))
class _Transformation(object):
EPSILON = 0.0000001 # arbitrary
def __init__(self, value=None):
if value:
self._matrix = tuple(value[:4])
self._translation = tuple(value[4:])
else:
self._matrix = (1., 0., 0., 1.)
self._translation = (0., 0.)
def __str__(self):
return repr(self._matrix)[:-1] + '; ' + repr(self._translation)[1:]
def image(self, point):
return Point(self._matrix[0]*point._x + self._matrix[1]*point._y + self._translation[0],
self._matrix[2]*point._x + self._matrix[3]*point._y + self._translation[1])
def inv(self):
detinv = 1. / self.det()
m = ( self._matrix[3] * detinv, -self._matrix[1] * detinv,
-self._matrix[2] * detinv, self._matrix[0] * detinv )
t = ( -m[0]*self._translation[0] - m[1]*self._translation[1],
-m[2]*self._translation[0] - m[3]*self._translation[1])
return _Transformation(m+t)
def __mul__(self, other):
m = (self._matrix[0] * other._matrix[0] + self._matrix[1] * other._matrix[2],
self._matrix[0] * other._matrix[1] + self._matrix[1] * other._matrix[3],
self._matrix[2] * other._matrix[0] + self._matrix[3] * other._matrix[2],
self._matrix[2] * other._matrix[1] + self._matrix[3] * other._matrix[3])
p = self.image( Point(other._translation[0], other._translation[1]) )
return _Transformation(m + (p.getX(), p.getY()))
def det(self):
return (self._matrix[0] * self._matrix[3] - self._matrix[1] * self._matrix[2])
def scale(self):
return _math.sqrt(abs(self.det()))
def scaleAndTranslate(self):
temp = self._matrix[0] - self._matrix[3] * (-1 if _mathMode else 1)
return (abs(temp) <= _Transformation.EPSILON and
abs(self._matrix[1]) <= _Transformation.EPSILON and
abs(self._matrix[2]) <= _Transformation.EPSILON)
def diagonalAndTranslate(self):
return (abs(self._matrix[1]) <= _Transformation.EPSILON and
abs(self._matrix[2]) <= _Transformation.EPSILON)
def translateOnly(self):
return (abs(self._matrix[1]) <= _Transformation.EPSILON and
abs(self._matrix[2]) <= _Transformation.EPSILON and
abs(self._matrix[0] - 1) <= _Transformation.EPSILON and
abs(self._matrix[3] - 1) <= _Transformation.EPSILON)
class Color(object):
"""A color representation.
A color can be specified by name or RGB value.
'Transparent' is used to denote the lack of a color.
See Color.AVAILABLE for a list of available color names.
"""
_colorValues = {
'aliceblue' : (240,248,255), 'antiquewhite' : (250,235,215),
'antiquewhite1' : (255,239,219), 'antiquewhite2' : (238,223,204),
'antiquewhite3' : (205,192,176), 'antiquewhite4' : (139,131,120),
'aquamarine' : (127,255,212), 'aquamarine1' : (127,255,212),
'aquamarine2' : (118,238,198), 'aquamarine3' : (102,205,170),
'aquamarine4' : ( 69,139,116), 'azure' : (240,255,255),
'azure1' : (240,255,255), 'azure2' : (224,238,238),
'azure3' : (193,205,205), 'azure4' : (131,139,139),
'beige' : (245,245,220), 'bisque' : (255,228,196),
'bisque1' : (255,228,196), 'bisque2' : (238,213,183),
'bisque3' : (205,183,158), 'bisque4' : (139,125,107),
'black' : ( 0, 0, 0), 'blanchedalmond' : (255,235,205),
'blue' : ( 0, 0,255), 'blue1' : ( 0, 0,255),
'blue2' : ( 0, 0,238), 'blue3' : ( 0, 0,205),
'blue4' : ( 0, 0,139), 'blueviolet' : (138, 43,226),
'brown' : (165, 42, 42), 'brown1' : (255, 64, 64),
'brown2' : (238, 59, 59), 'brown3' : (205, 51, 51),
'brown4' : (139, 35, 35), 'burlywood' : (222,184,135),
'burlywood1' : (255,211,155), 'burlywood2' : (238,197,145),
'burlywood3' : (205,170,125), 'burlywood4' : (139,115, 85),
'cadetblue' : ( 95,158,160), 'cadetblue1' : (152,245,255),
'cadetblue2' : (142,229,238), 'cadetblue3' : (122,197,205),
'cadetblue4' : ( 83,134,139), 'chartreuse' : (127,255, 0),
'chartreuse1' : (127,255, 0), 'chartreuse2' : (118,238, 0),
'chartreuse3' : (102,205, 0), 'chartreuse4' : ( 69,139, 0),
'chocolate' : (210,105, 30), 'chocolate1' : (255,127, 36),
'chocolate2' : (238,118, 33), 'chocolate3' : (205,102, 29),
'chocolate4' : (139, 69, 19), 'coral' : (255,127, 80),
'coral1' : (255,114, 86), 'coral2' : (238,106, 80),
'coral3' : (205, 91, 69), 'coral4' : (139, 62, 47),
'cornflowerblue' : (100,149,237), 'cornsilk' : (255,248,220),
'cornsilk1' : (255,248,220), 'cornsilk2' : (238,232,205),
'cornsilk3' : (205,200,177), 'cornsilk4' : (139,136,120),
'cyan' : ( 0,255,255), 'cyan1' : ( 0,255,255),
'cyan2' : ( 0,238,238), 'cyan3' : ( 0,205,205),
'cyan4' : ( 0,139,139), 'darkblue' : ( 0, 0,139),
'darkcyan' : ( 0,139,139), 'darkgoldenrod' : (184,134, 11),
'darkgoldenrod1' : (255,185, 15), 'darkgoldenrod2' : (238,173, 14),
'darkgoldenrod3' : (205,149, 12), 'darkgoldenrod4' : (139,101, 8),
'darkgray' : (169,169,169), 'darkgreen' : ( 0,100, 0),
'darkgrey' : (169,169,169), 'darkkhaki' : (189,183,107),
'darkmagenta' : (139, 0,139), 'darkolivegreen' : ( 85,107, 47),
'darkolivegreen1' : (202,255,112), 'darkolivegreen2' : (188,238,104),
'darkolivegreen3' : (162,205, 90), 'darkolivegreen4' : (110,139, 61),
'darkorange' : (255,140, 0), 'darkorange1' : (255,127, 0),
'darkorange2' : (238,118, 0), 'darkorange3' : (205,102, 0),
'darkorange4' : (139, 69, 0), 'darkorchid' : (153, 50,204),
'darkorchid1' : (191, 62,255), 'darkorchid2' : (178, 58,238),
'darkorchid3' : (154, 50,205), 'darkorchid4' : (104, 34,139),
'darkred' : (139, 0, 0), 'darksalmon' : (233,150,122),
'darkseagreen' : (143,188,143), 'darkseagreen1' : (193,255,193),
'darkseagreen2' : (180,238,180), 'darkseagreen3' : (155,205,155),
'darkseagreen4' : (105,139,105), 'darkslateblue' : ( 72, 61,139),
'darkslategray' : ( 47, 79, 79), 'darkslategray1' : (151,255,255),
'darkslategray2' : (141,238,238), 'darkslategray3' : (121,205,205),
'darkslategray4' : ( 82,139,139), 'darkslategrey' : ( 47, 79, 79),
'darkturquoise' : ( 0,206,209), 'darkviolet' : (148, 0,211),
'deeppink' : (255, 20,147), 'deeppink1' : (255, 20,147),
'deeppink2' : (238, 18,137), 'deeppink3' : (205, 16,118),
'deeppink4' : (139, 10, 80), 'deepskyblue' : ( 0,191,255),
'deepskyblue1' : ( 0,191,255), 'deepskyblue2' : ( 0,178,238),
'deepskyblue3' : ( 0,154,205), 'deepskyblue4' : ( 0,104,139),
'dimgray' : (105,105,105), 'dimgrey' : (105,105,105),
'dodgerblue' : ( 30,144,255), 'dodgerblue1' : ( 30,144,255),
'dodgerblue2' : ( 28,134,238), 'dodgerblue3' : ( 24,116,205),
'dodgerblue4' : ( 16, 78,139), 'firebrick' : (178, 34, 34),
'firebrick1' : (255, 48, 48), 'firebrick2' : (238, 44, 44),
'firebrick3' : (205, 38, 38), 'firebrick4' : (139, 26, 26),
'floralwhite' : (255,250,240), 'forestgreen' : ( 34,139, 34),
'gainsboro' : (220,220,220), 'ghostwhite' : (248,248,255),
'gold' : (255,215, 0), 'gold1' : (255,215, 0),
'gold2' : (238,201, 0), 'gold3' : (205,173, 0),
'gold4' : (139,117, 0), 'goldenrod' : (218,165, 32),
'goldenrod1' : (255,193, 37), 'goldenrod2' : (238,180, 34),
'goldenrod3' : (205,155, 29), 'goldenrod4' : (139,105, 20),
'gray' : (190,190,190), 'gray0' : ( 0, 0, 0),
'gray1' : ( 3, 3, 3), 'gray10' : ( 26, 26, 26),
'gray100' : (255,255,255), 'gray11' : ( 28, 28, 28),
'gray12' : ( 31, 31, 31), 'gray13' : ( 33, 33, 33),
'gray14' : ( 36, 36, 36), 'gray15' : ( 38, 38, 38),
'gray16' : ( 41, 41, 41), 'gray17' : ( 43, 43, 43),
'gray18' : ( 46, 46, 46), 'gray19' : ( 48, 48, 48),
'gray2' : ( 5, 5, 5), 'gray20' : ( 51, 51, 51),
'gray21' : ( 54, 54, 54), 'gray22' : ( 56, 56, 56),
'gray23' : ( 59, 59, 59), 'gray24' : ( 61, 61, 61),
'gray25' : ( 64, 64, 64), 'gray26' : ( 66, 66, 66),
'gray27' : ( 69, 69, 69), 'gray28' : ( 71, 71, 71),
'gray29' : ( 74, 74, 74), 'gray3' : ( 8, 8, 8),
'gray30' : ( 77, 77, 77), 'gray31' : ( 79, 79, 79),
'gray32' : ( 82, 82, 82), 'gray33' : ( 84, 84, 84),
'gray34' : ( 87, 87, 87), 'gray35' : ( 89, 89, 89),
'gray36' : ( 92, 92, 92), 'gray37' : ( 94, 94, 94),
'gray38' : ( 97, 97, 97), 'gray39' : ( 99, 99, 99),
'gray4' : ( 10, 10, 10), 'gray40' : (102,102,102),
'gray41' : (105,105,105), 'gray42' : (107,107,107),
'gray43' : (110,110,110), 'gray44' : (112,112,112),
'gray45' : (115,115,115), 'gray46' : (117,117,117),
'gray47' : (120,120,120), 'gray48' : (122,122,122),
'gray49' : (125,125,125), 'gray5' : ( 13, 13, 13),
'gray50' : (127,127,127), 'gray51' : (130,130,130),
'gray52' : (133,133,133), 'gray53' : (135,135,135),
'gray54' : (138,138,138), 'gray55' : (140,140,140),
'gray56' : (143,143,143), 'gray57' : (145,145,145),
'gray58' : (148,148,148), 'gray59' : (150,150,150),
'gray6' : ( 15, 15, 15), 'gray60' : (153,153,153),
'gray61' : (156,156,156), 'gray62' : (158,158,158),
'gray63' : (161,161,161), 'gray64' : (163,163,163),
'gray65' : (166,166,166), 'gray66' : (168,168,168),
'gray67' : (171,171,171), 'gray68' : (173,173,173),
'gray69' : (176,176,176), 'gray7' : ( 18, 18, 18),
'gray70' : (179,179,179), 'gray71' : (181,181,181),
'gray72' : (184,184,184), 'gray73' : (186,186,186),
'gray74' : (189,189,189), 'gray75' : (191,191,191),
'gray76' : (194,194,194), 'gray77' : (196,196,196),
'gray78' : (199,199,199), 'gray79' : (201,201,201),
'gray8' : ( 20, 20, 20), 'gray80' : (204,204,204),
'gray81' : (207,207,207), 'gray82' : (209,209,209),
'gray83' : (212,212,212), 'gray84' : (214,214,214),
'gray85' : (217,217,217), 'gray86' : (219,219,219),
'gray87' : (222,222,222), 'gray88' : (224,224,224),
'gray89' : (227,227,227), 'gray9' : ( 23, 23, 23),
'gray90' : (229,229,229), 'gray91' : (232,232,232),
'gray92' : (235,235,235), 'gray93' : (237,237,237),
'gray94' : (240,240,240), 'gray95' : (242,242,242),
'gray96' : (245,245,245), 'gray97' : (247,247,247),
'gray98' : (250,250,250), 'gray99' : (252,252,252),
'green' : ( 0,255, 0), 'green1' : ( 0,255, 0),
'green2' : ( 0,238, 0), 'green3' : ( 0,205, 0),
'green4' : ( 0,139, 0), 'greenyellow' : (173,255, 47),
'grey' : (190,190,190), 'grey0' : ( 0, 0, 0),
'grey1' : ( 3, 3, 3), 'grey10' : ( 26, 26, 26),
'grey100' : (255,255,255), 'grey11' : ( 28, 28, 28),
'grey12' : ( 31, 31, 31), 'grey13' : ( 33, 33, 33),
'grey14' : ( 36, 36, 36), 'grey15' : ( 38, 38, 38),
'grey16' : ( 41, 41, 41), 'grey17' : ( 43, 43, 43),
'grey18' : ( 46, 46, 46), 'grey19' : ( 48, 48, 48),
'grey2' : ( 5, 5, 5), 'grey20' : ( 51, 51, 51),
'grey21' : ( 54, 54, 54), 'grey22' : ( 56, 56, 56),
'grey23' : ( 59, 59, 59), 'grey24' : ( 61, 61, 61),
'grey25' : ( 64, 64, 64), 'grey26' : ( 66, 66, 66),
'grey27' : ( 69, 69, 69), 'grey28' : ( 71, 71, 71),
'grey29' : ( 74, 74, 74), 'grey3' : ( 8, 8, 8),
'grey30' : ( 77, 77, 77), 'grey31' : ( 79, 79, 79),
'grey32' : ( 82, 82, 82), 'grey33' : ( 84, 84, 84),
'grey34' : ( 87, 87, 87), 'grey35' : ( 89, 89, 89),
'grey36' : ( 92, 92, 92), 'grey37' : ( 94, 94, 94),
'grey38' : ( 97, 97, 97), 'grey39' : ( 99, 99, 99),
'grey4' : ( 10, 10, 10), 'grey40' : (102,102,102),
'grey41' : (105,105,105), 'grey42' : (107,107,107),
'grey43' : (110,110,110), 'grey44' : (112,112,112),
'grey45' : (115,115,115), 'grey46' : (117,117,117),
'grey47' : (120,120,120), 'grey48' : (122,122,122),
'grey49' : (125,125,125), 'grey5' : ( 13, 13, 13),
'grey50' : (127,127,127), 'grey51' : (130,130,130),
'grey52' : (133,133,133), 'grey53' : (135,135,135),
'grey54' : (138,138,138), 'grey55' : (140,140,140),
'grey56' : (143,143,143), 'grey57' : (145,145,145),
'grey58' : (148,148,148), 'grey59' : (150,150,150),
'grey6' : ( 15, 15, 15), 'grey60' : (153,153,153),
'grey61' : (156,156,156), 'grey62' : (158,158,158),
'grey63' : (161,161,161), 'grey64' : (163,163,163),
'grey65' : (166,166,166), 'grey66' : (168,168,168),
'grey67' : (171,171,171), 'grey68' : (173,173,173),
'grey69' : (176,176,176), 'grey7' : ( 18, 18, 18),
'grey70' : (179,179,179), 'grey71' : (181,181,181),
'grey72' : (184,184,184), 'grey73' : (186,186,186),
'grey74' : (189,189,189), 'grey75' : (191,191,191),
'grey76' : (194,194,194), 'grey77' : (196,196,196),
'grey78' : (199,199,199), 'grey79' : (201,201,201),
'grey8' : ( 20, 20, 20), 'grey80' : (204,204,204),
'grey81' : (207,207,207), 'grey82' : (209,209,209),
'grey83' : (212,212,212), 'grey84' : (214,214,214),
'grey85' : (217,217,217), 'grey86' : (219,219,219),
'grey87' : (222,222,222), 'grey88' : (224,224,224),
'grey89' : (227,227,227), 'grey9' : ( 23, 23, 23),
'grey90' : (229,229,229), 'grey91' : (232,232,232),
'grey92' : (235,235,235), 'grey93' : (237,237,237),
'grey94' : (240,240,240), 'grey95' : (242,242,242),
'grey96' : (245,245,245), 'grey97' : (247,247,247),
'grey98' : (250,250,250), 'grey99' : (252,252,252),
'honeydew' : (240,255,240), 'honeydew1' : (240,255,240),
'honeydew2' : (224,238,224), 'honeydew3' : (193,205,193),
'honeydew4' : (131,139,131), 'hotpink' : (255,105,180),
'hotpink1' : (255,110,180), 'hotpink2' : (238,106,167),
'hotpink3' : (205, 96,144), 'hotpink4' : (139, 58, 98),
'indianred' : (205, 92, 92), 'indianred1' : (255,106,106),
'indianred2' : (238, 99, 99), 'indianred3' : (205, 85, 85),
'indianred4' : (139, 58, 58), 'ivory' : (255,255,240),
'ivory1' : (255,255,240), 'ivory2' : (238,238,224),
'ivory3' : (205,205,193), 'ivory4' : (139,139,131),
'khaki' : (240,230,140), 'khaki1' : (255,246,143),
'khaki2' : (238,230,133), 'khaki3' : (205,198,115),
'khaki4' : (139,134, 78), 'lavender' : (230,230,250),
'lavenderblush' : (255,240,245), 'lavenderblush1' : (255,240,245),
'lavenderblush2' : (238,224,229), 'lavenderblush3' : (205,193,197),
'lavenderblush4' : (139,131,134), 'lawngreen' : (124,252, 0),
'lemonchiffon' : (255,250,205), 'lemonchiffon1' : (255,250,205),
'lemonchiffon2' : (238,233,191), 'lemonchiffon3' : (205,201,165),
'lemonchiffon4' : (139,137,112), 'lightblue' : (173,216,230),
'lightblue1' : (191,239,255), 'lightblue2' : (178,223,238),
'lightblue3' : (154,192,205), 'lightblue4' : (104,131,139),
'lightcoral' : (240,128,128), 'lightcyan' : (224,255,255),
'lightcyan1' : (224,255,255), 'lightcyan2' : (209,238,238),
'lightcyan3' : (180,205,205), 'lightcyan4' : (122,139,139),
'lightgoldenrod' : (238,221,130), 'lightgoldenrod1' : (255,236,139),
'lightgoldenrod2' : (238,220,130), 'lightgoldenrod3' : (205,190,112),
'lightgoldenrod4' : (139,129, 76), 'lightgoldenrodyellow' : (250,250,210),
'lightgray' : (211,211,211), 'lightgreen' : (144,238,144),
'lightgrey' : (211,211,211), 'lightpink' : (255,182,193),
'lightpink1' : (255,174,185), 'lightpink2' : (238,162,173),
'lightpink3' : (205,140,149), 'lightpink4' : (139, 95,101),
'lightsalmon' : (255,160,122), 'lightsalmon1' : (255,160,122),
'lightsalmon2' : (238,149,114), 'lightsalmon3' : (205,129, 98),
'lightsalmon4' : (139, 87, 66), 'lightseagreen' : ( 32,178,170),
'lightskyblue' : (135,206,250), 'lightskyblue1' : (176,226,255),
'lightskyblue2' : (164,211,238), 'lightskyblue3' : (141,182,205),
'lightskyblue4' : ( 96,123,139), 'lightslateblue' : (132,112,255),
'lightslategray' : (119,136,153), 'lightslategrey' : (119,136,153),
'lightsteelblue' : (176,196,222), 'lightsteelblue1' : (202,225,255),
'lightsteelblue2' : (188,210,238), 'lightsteelblue3' : (162,181,205),
'lightsteelblue4' : (110,123,139), 'lightyellow' : (255,255,224),
'lightyellow1' : (255,255,224), 'lightyellow2' : (238,238,209),
'lightyellow3' : (205,205,180), 'lightyellow4' : (139,139,122),
'limegreen' : ( 50,205, 50), 'linen' : (250,240,230),
'magenta' : (255, 0,255), 'magenta1' : (255, 0,255),
'magenta2' : (238, 0,238), 'magenta3' : (205, 0,205),
'magenta4' : (139, 0,139), 'maroon' : (176, 48, 96),
'maroon1' : (255, 52,179), 'maroon2' : (238, 48,167),
'maroon3' : (205, 41,144), 'maroon4' : (139, 28, 98),
'mediumaquamarine' : (102,205,170), 'mediumblue' : ( 0, 0,205),
'mediumorchid' : (186, 85,211), 'mediumorchid1' : (224,102,255),
'mediumorchid2' : (209, 95,238), 'mediumorchid3' : (180, 82,205),
'mediumorchid4' : (122, 55,139), 'mediumpurple' : (147,112,219),
'mediumpurple1' : (171,130,255), 'mediumpurple2' : (159,121,238),
'mediumpurple3' : (137,104,205), 'mediumpurple4' : ( 93, 71,139),
'mediumseagreen' : ( 60,179,113), 'mediumslateblue' : (123,104,238),
'mediumspringgreen' : ( 0,250,154), 'mediumturquoise' : ( 72,209,204),
'mediumvioletred' : (199, 21,133), 'midnightblue' : ( 25, 25,112),
'mintcream' : (245,255,250), 'mistyrose' : (255,228,225),
'mistyrose1' : (255,228,225), 'mistyrose2' : (238,213,210),
'mistyrose3' : (205,183,181), 'mistyrose4' : (139,125,123),
'moccasin' : (255,228,181), 'navajowhite' : (255,222,173),
'navajowhite1' : (255,222,173), 'navajowhite2' : (238,207,161),
'navajowhite3' : (205,179,139), 'navajowhite4' : (139,121, 94),
'navy' : ( 0, 0,128), 'navyblue' : ( 0, 0,128),
'oldlace' : (253,245,230), 'olivedrab' : (107,142, 35),
'olivedrab1' : (192,255, 62), 'olivedrab2' : (179,238, 58),
'olivedrab3' : (154,205, 50), 'olivedrab4' : (105,139, 34),
'orange' : (255,165, 0), 'orange1' : (255,165, 0),
'orange2' : (238,154, 0), 'orange3' : (205,133, 0),
'orange4' : (139, 90, 0), 'orangered' : (255, 69, 0),
'orangered1' : (255, 69, 0), 'orangered2' : (238, 64, 0),
'orangered3' : (205, 55, 0), 'orangered4' : (139, 37, 0),
'orchid' : (218,112,214), 'orchid1' : (255,131,250),
'orchid2' : (238,122,233), 'orchid3' : (205,105,201),
'orchid4' : (139, 71,137), 'palegoldenrod' : (238,232,170),
'palegreen' : (152,251,152), 'palegreen1' : (154,255,154),
'palegreen2' : (144,238,144), 'palegreen3' : (124,205,124),
'palegreen4' : ( 84,139, 84), 'paleturquoise' : (175,238,238),
'paleturquoise1' : (187,255,255), 'paleturquoise2' : (174,238,238),
'paleturquoise3' : (150,205,205), 'paleturquoise4' : (102,139,139),
'palevioletred' : (219,112,147), 'palevioletred1' : (255,130,171),
'palevioletred2' : (238,121,159), 'palevioletred3' : (205,104,137),
'palevioletred4' : (139, 71, 93), 'papayawhip' : (255,239,213),
'peachpuff' : (255,218,185), 'peachpuff1' : (255,218,185),
'peachpuff2' : (238,203,173), 'peachpuff3' : (205,175,149),
'peachpuff4' : (139,119,101), 'peru' : (205,133, 63),
'pink' : (255,192,203), 'pink1' : (255,181,197),
'pink2' : (238,169,184), 'pink3' : (205,145,158),
'pink4' : (139, 99,108), 'plum' : (221,160,221),
'plum1' : (255,187,255), 'plum2' : (238,174,238),
'plum3' : (205,150,205), 'plum4' : (139,102,139),
'powderblue' : (176,224,230), 'purple' : (160, 32,240),
'purple1' : (155, 48,255), 'purple2' : (145, 44,238),
'purple3' : (125, 38,205), 'purple4' : ( 85, 26,139),
'red' : (255, 0, 0), 'red1' : (255, 0, 0),
'red2' : (238, 0, 0), 'red3' : (205, 0, 0),
'red4' : (139, 0, 0), 'rosybrown' : (188,143,143),
'rosybrown1' : (255,193,193), 'rosybrown2' : (238,180,180),
'rosybrown3' : (205,155,155), 'rosybrown4' : (139,105,105),
'royalblue' : ( 65,105,225), 'royalblue1' : ( 72,118,255),
'royalblue2' : ( 67,110,238), 'royalblue3' : ( 58, 95,205),
'royalblue4' : ( 39, 64,139), 'saddlebrown' : (139, 69, 19),
'salmon' : (250,128,114), 'salmon1' : (255,140,105),
'salmon2' : (238,130, 98), 'salmon3' : (205,112, 84),
'salmon4' : (139, 76, 57), 'sandybrown' : (244,164, 96),
'seagreen' : ( 46,139, 87), 'seagreen1' : ( 84,255,159),
'seagreen2' : ( 78,238,148), 'seagreen3' : ( 67,205,128),
'seagreen4' : ( 46,139, 87), 'seashell' : (255,245,238),
'seashell1' : (255,245,238), 'seashell2' : (238,229,222),
'seashell3' : (205,197,191), 'seashell4' : (139,134,130),
'sienna' : (160, 82, 45), 'sienna1' : (255,130, 71),
'sienna2' : (238,121, 66), 'sienna3' : (205,104, 57),
'sienna4' : (139, 71, 38), 'skyblue' : (135,206,235),
'skyblue1' : (135,206,255), 'skyblue2' : (126,192,238),
'skyblue3' : (108,166,205), 'skyblue4' : ( 74,112,139),
'slateblue' : (106, 90,205), 'slateblue1' : (131,111,255),
'slateblue2' : (122,103,238), 'slateblue3' : (105, 89,205),
'slateblue4' : ( 71, 60,139), 'slategray' : (112,128,144),
'slategray1' : (198,226,255), 'slategray2' : (185,211,238),
'slategray3' : (159,182,205), 'slategray4' : (108,123,139),
'slategrey' : (112,128,144), 'snow' : (255,250,250),
'snow1' : (255,250,250), 'snow2' : (238,233,233),
'snow3' : (205,201,201), 'snow4' : (139,137,137),
'springgreen' : ( 0,255,127), 'springgreen1' : ( 0,255,127),
'springgreen2' : ( 0,238,118), 'springgreen3' : ( 0,205,102),
'springgreen4' : ( 0,139, 69), 'steelblue' : ( 70,130,180),
'steelblue1' : ( 99,184,255), 'steelblue2' : ( 92,172,238),
'steelblue3' : ( 79,148,205), 'steelblue4' : ( 54,100,139),
'tan' : (210,180,140), 'tan1' : (255,165, 79),
'tan2' : (238,154, 73), 'tan3' : (205,133, 63),
'tan4' : (139, 90, 43), 'thistle' : (216,191,216),
'thistle1' : (255,225,255), 'thistle2' : (238,210,238),
'thistle3' : (205,181,205), 'thistle4' : (139,123,139),
'tomato' : (255, 99, 71), 'tomato1' : (255, 99, 71),
'tomato2' : (238, 92, 66), 'tomato3' : (205, 79, 57),
'tomato4' : (139, 54, 38), 'turquoise' : ( 64,224,208),
'turquoise1' : ( 0,245,255), 'turquoise2' : ( 0,229,238),
'turquoise3' : ( 0,197,205), 'turquoise4' : ( 0,134,139),
'violet' : (238,130,238), 'violetred' : (208, 32,144),
'violetred1' : (255, 62,150), 'violetred2' : (238, 58,140),
'violetred3' : (205, 50,120), 'violetred4' : (139, 34, 82),
'wheat' : (245,222,179), 'wheat1' : (255,231,186),
'wheat2' : (238,216,174), 'wheat3' : (205,186,150),
'wheat4' : (139,126,102), 'white' : (255,255,255),
'whitesmoke' : (245,245,245), 'yellow' : (255,255, 0),
'yellow1' : (255,255, 0), 'yellow2' : (238,238, 0),
'yellow3' : (205,205, 0), 'yellow4' : (139,139, 0),
'yellowgreen' : (154,205, 50),
}
AVAILABLE = list(_colorValues.keys())
AVAILABLE.sort()
def randomColor():
"""Return a random color.
This static method should be invoked as Color.randomColor().
"""
return Color( (_random.randint(0, 255), _random.randint(0, 255), _random.randint(0, 255)) )
randomColor = staticmethod(randomColor)
def __init__(self, colorChoice='white'):
"""Create a new Color instance (default 'white').
The parameter can be either:
- a string with the name of the color
- an (r,g,b) tuple
- an existing Color instance (which will be cloned)
"""
# we intentionally have Cavases and Drawable objects using a color
# register with the color instance, so that when the color is
# mutated, the object can be informed that it has changed
# registration is for each (user,role) pair, so a fillable that
# is using color as both fill and border is registered twice.
self._users = set()
if isinstance(colorChoice, basestring):
try:
self.setByName(colorChoice)
except ValueError:
raise
elif isinstance(colorChoice, tuple):
try:
self.setByValue(colorChoice)
except ValueError:
raise
elif isinstance(colorChoice, Color):
self._colorName = colorChoice._colorName
self._transparent = colorChoice._transparent
self._colorValue = colorChoice._colorValue
else:
raise TypeError('invalid color specification')
def __deepcopy__(self, memo={}):
"""This copy avoids duplicating the _users registry."""
c = Color(self)
memo[id(self)] = c
return c
def setByName(self, colorName):
"""Set the color to colorName.
colorName a string representing a valid name
('Transparent' designates the lack of color)
"""
if not isinstance(colorName, basestring):
raise TypeError('string expected as color name')
cleanName = colorName.lower().replace(' ','')
if cleanName == 'transparent':
if self._isCanvasBackground():
raise ValueError('canvas background cannot be transparent')
self._transparent = True
self._colorValue = (0, 0, 0)
else:
if cleanName not in Color._colorValues:
msg = colorName + ' is not a valid color name'
raise ValueError(msg)
self._colorValue = Color._colorValues[cleanName]
self._transparent = False
self._colorName = colorName # use original string format
self._informUsers()
def getColorName(self):
"""Return the name of the color.
If the color was set by RGB value, it returns 'Custom'.
"""
return self._colorName
def setByValue(self, rgbTuple):
"""Set the color to the given tuple of (red, green, blue) values."""
if not isinstance(rgbTuple, tuple):
raise TypeError('(r,g,b) tuple expected')
if len(rgbTuple)!=3:
raise ValueError('(r,g,b) tuple must have three components')
for val in rgbTuple:
if not isinstance(val, (int, float)):
raise TypeError('tuple entries must be numbers')
elif not 0 <= val <= 255:
raise ValueError('tuple entries must be from 0 to 255')
self._transparent = False
self._colorName = 'Custom'
self._colorValue = rgbTuple
self._informUsers()
def getColorValue(self):
"""Return a tuple of the (red, green, blue) color components."""
return (self._colorValue[0], self._colorValue[1], self._colorValue[2])
def isTransparent(self):
"""Return True if the current color is transparent."""
return self._transparent
def __repr__(self):
"""Return the name of the color, if named.
Otherwise return the (r,g,b) value.
"""
if self._colorName == 'Custom':
return self._colorValue.__repr__()
else:
return self._colorName
def __eq__(self, other):
"""Return true if the two colors have equivalent value."""
return ( (self._transparent, self._colorValue) ==
(other._transparent, other._colorValue) )
def __ne__(self, other):
"""Return true if the two colors do not have equivalent value."""
return not self == other
def _register(self, user, role):
"""Register a user with this Color instance."""
if user not in self._users:
self._users.add( (user,role) )
def _unregister(self, user, role):
"""Unregister a user from this Color instance."""
self._users.discard( (user,role) )
def _isCanvasBackground(self):
"""Check to see if this Color instance is currently registered with a Canvas."""
for (user,role) in self._users:
if isinstance(user, Canvas):
return True
return False
def _informUsers(self):
"""Inform registered users that the Color instance is mutated."""
temp = Color(self)
for (user,role) in self._users:
user._update({role : temp})
@staticmethod
def _getTkColor(color):
if color._transparent:
return ''
return '#%04X%04X%04X' % (256*color.getColorValue()[0], 256*color.getColorValue()[1], 256*color.getColorValue()[2])
class _GraphicsContainer(object):
def __init__(self):
self._contents = []
def __contains__(self, obj):
"""Return True if obj is currently in the container; False otherwise."""
return obj in self._contents
def add(self, drawable):
"""Add the Drawable object to the container."""
# not doing error checking here, as we want tailored messages for Canvas and Layer
self._contents.append(drawable)
if self in _graphicsManager._frontHierarchy:
if _debug >= 2: print('adding drawable to "rendered" graphics container')
_graphicsManager.beginRefresh()
cacheParent = _graphicsManager._drawParent # probably None. But not quite sure
cls = Canvas if isinstance(self, Canvas) else Layer # although possible subclass of Layer
_graphicsManager._drawParent = (self,cls)
drawable._draw()
_graphicsManager._drawParent = cacheParent
_graphicsManager.completeRefresh()
def remove(self, drawable):
"""Remove the Drawable object from the container."""
# not doing error checking here, as we want tailored messages for Canvas and Layer
self._contents.remove(drawable)
if drawable in _graphicsManager._frontHierarchy:
cls = Canvas if isinstance(self, Canvas) else Layer
_graphicsManager.beginRefresh()
childTuple = _graphicsManager._frontHierarchy.findChildTuple((self,cls), drawable)
if _debug >= 1:
print('_frontHierarchy.removeLink: ' + str( (self,cls) ) + ' ' + str(childTuple))
_graphicsManager._frontHierarchy.removeLink((self,cls), childTuple)
_graphicsManager.addCommandToQueue(('object removed', (self,cls), childTuple))
_graphicsManager.completeRefresh()
def clear(self):
"""Remove all objects from the container."""
# Note: odd design, as we assume that any child class of this
# has _frozen attribute defined as well as either a
# freeze/unfreeze pair or a setAutoRefresh. This is designed
# specifically because Layers inherit this from Drawable
# context while Canvas has its own autoRefresh interface
wasFrozen = self._frozen
if not wasFrozen: # temporarily freeze it
try:
self.freeze() # presumably a Layer
except AttributeError:
self.setAutoRefresh(False) # presumably a Canvas
contents = list(self._contents) # intentional clone since remove mutates list
for drawable in contents:
self.remove(drawable)
if not wasFrozen: # restore unfrozen state
try:
self.unfreeze() # presumably a Layer
except AttributeError:
self.setAutoRefresh(True) # presumably a Canvas
def getContents(self):
"""Return a list of the container's contents, sorted by decreasing depth."""
# this is not currently used by our code, but there for users
return sorted(self._contents, key=Drawable.getDepth, reverse=True)
def _wrapUtility(cls):
if _debug >= 2: print('_wrapUtility being called on class ' + str(cls))
classDict = cls.__dict__
if '_internalDraw' not in classDict: # not alreadly wrapped
if '_draw' in classDict:
if _debug >= 2: print('_wrapUtility: wrap was required')
internalDraw = cls._draw
setattr(cls, '_internalDraw', internalDraw)
#---------------------------------------------------------------------------
# defining closure to wrap the original _draw while identifying proper class
def drawClosure(self):
# Note: cls and internalDraw taken from the closure
if _debug >= 2: print(str(cls) + ' draw wrapper called on ' + str(self))
parent = _graphicsManager._drawParent
if not parent:
raise GraphicsError('_draw should not be directly called', True)
siblings = _graphicsManager._drawChildren
if siblings is not None:
siblings.append( (self,cls) )
known = self in _graphicsManager._frontHierarchy # query this before adding to hierarchy
if _debug >= 1:
print('
_frontHierarchy.addLink: ' + str(parent) + ' ' + str( (self,cls) ))
_graphicsManager._frontHierarchy.addLink(parent, (self,cls))
if not known:
_graphicsManager.addCommandToQueue(('update', self, self._getProperties())) # presend all properties
_graphicsManager.addCommandToQueue(('object added', parent, (self,cls)))
if not known:
if _debug >= 2: print('about to call original _draw() for ' + str(self))
_graphicsManager._drawParent = (self,cls)
internalDraw(self) # the original wrapped function, taken from closure
_graphicsManager._drawParent = parent
if _debug >= 2: print('draw wrapper call ending for ' + str(self))
# end of closure
#---------------------------------------------------------------------------
setattr(cls, '_draw', drawClosure)
# if _internalDraw exists, then parents are already wrapped as well,
# but we cannot be sure of there is no _internalDraw nor _draw, so let's recurse
for base in cls.__bases__:
if issubclass(base, Drawable):
_wrapUtility(base)
# Drawable Hierarchy
class Drawable(_EventTrigger):
"""An object that can be drawn to a graphics canvas."""
def __init__(self, reference=None):
"""Create a Drawable instance.
referencePoint local reference point for scaling, rotating and flipping
(default Point(0,0) )
"""
_EventTrigger.__init__(self)
_wrapUtility(self.__class__)
if reference is not None:
if not isinstance(reference, Point):
raise TypeError('reference point must be a Point instance')
else:
reference = Point()
self._reference = reference
self._transform = _Transformation()
self._depth = 50
self._frozen = False
def __deepcopy__(self, memo={}):
"""This provides underlying support for clone()."""
# We use Drawable.__deepcopy__ to do all the real work.
# Subtypes can customize as needed.
temp = self.__class__.__new__(self.__class__)
memo[id(self)] = temp
for k,v in self.__dict__.items():
temp.__dict__[k] = _copy.deepcopy(v, memo)
return temp
# TODO: get rid of this. temporary hack for 3.0 issue and comparing chains
def __lt__(self, other):
return id(self) < id(other)
def isFrozen(self):
"""Returns True if currently frozen; False otherwise."""
return self._frozen
def freeze(self):
"""Freeze the current object (if not already frozen).
For an object that is already rendered, when frozen, any
further changes to it will not be rendered until such time
when unfrozen() is called.
However, if unrendered, when added to a canvas or layer, this
object will be rendered with its most current properties, even
if currently frozen.
"""
if not self._frozen:
self._frozen = True
if self in _graphicsManager._frontHierarchy:
_graphicsManager.beginRefresh()
_graphicsManager.addCommandToQueue(('freeze', self))
_graphicsManager.completeRefresh()
def unfreeze(self):
"""Unfreeze the current object (if currently frozen).
When unfrozen, all changes that were made since the most
recent call to freeze() will be rendered.
"""
if self._frozen:
self._frozen = False
if self in _graphicsManager._frontHierarchy:
_graphicsManager.beginRefresh()
_graphicsManager.addCommandToQueue(('unfreeze', self))
_graphicsManager.completeRefresh()
def move(self, dx, dy):
"""Move the object dx units along X-axis and dy units along Y-axis.
For the default coordinate system, positive dx is rightward and
negative is leftward; positive dy is downard and negative is upward.
"""
if not isinstance(dx, (int,float)):
raise TypeError('dx must be numeric')
if not isinstance(dy, (int,float)):
raise TypeError('dy must be numeric')
self._transform = _Transformation( (1.,0.,0.,1.,dx,dy)) * self._transform
self._update({'transformation': self._transform})
def moveTo(self, x, y):
"""Move the object to align its reference point with (x,y)"""
if not isinstance(x, (int,float)):
raise TypeError('x must be numeric')
if not isinstance(y, (int,float)):
raise TypeError('y must be numeric')
curRef = self.getReferencePoint()
self.move(x-curRef.getX(), y-curRef.getY())
def rotate(self, angle):
"""Rotate the object around its current reference point.
angle number of degrees of clockwise rotation
"""
if not isinstance(angle, (int,float)):
raise TypeError('angle must be numeric')
angle = -_math.pi*angle/180.
p = self._localToGlobal(self._reference)
trans = _Transformation((1.,0.,0.,1.)+p.get())
rot = _Transformation((_math.cos(angle),_math.sin(angle),
-_math.sin(angle),_math.cos(angle),0.,0.))
self._transform = trans*(rot*(trans.inv()*self._transform))
self._update({'transformation': self._transform})
def scale(self, factor):
"""Scale the object relative to its current reference point.
factor scale is multiplied by this number (must be positive)
"""
if not isinstance(factor, (int,float)):
raise TypeError('scaling factor must be a positive number')
if factor <= 0:
raise ValueError('scaling factor must be a positive number')
p = self._localToGlobal(self._reference)
trans = _Transformation((1.,0.,0.,1.)+p.get())
sca = _Transformation((factor,0.,0.,factor,0.,0.))
self._transform = trans*(sca*(trans.inv()*self._transform))
self._update({'transformation': self._transform})
def stretch(self, xFactor, yFactor, angle=0):
"""Stretch the shape in mutltiple direction.
By default the x-axis is scaled by a factor of xFactor and the
y-axis is scaled by a factor of yFactor. The optional
parameter rotates the directions that the streching is performed
along.
"""
if not isinstance(xFactor, (int,float)) or not isinstance(yFactor, (int,float)):
raise TypeError('stretch factor must be a positive number')
if xFactor<=0 or yFactor<=0:
raise ValueError('stretch factor must be a positive number')
p = self._localToGlobal(self._reference)
trans = _Transformation((1.,0.,0.,1.)+p.get())
rot = _Transformation((_math.cos(angle),_math.sin(angle),
-_math.sin(angle),_math.cos(angle),0.,0.))
rotinv = rot.inv()
sca = _Transformation((xFactor,0.,0.,yFactor,0.,0.))
self._transform = trans*(rotinv*(sca*(rot*(trans.inv()*self._transform))))
self._update({'transformation': self._transform})
def flip(self, angle=0):
"""Flip the object reflected about its current reference point.
By default the flip is a left-to-right flip with a vertical axis of symmetry.
angle a clockwise rotation of the axis of symmetry away from vertical
"""
if not isinstance(angle, (int,float)):
raise TypeError('angle must be numeric')
angle = _math.pi*angle/180.
p = self._localToGlobal(self._reference)
trans = _Transformation((1.,0.,0.,1.)+p.get())
rot = _Transformation((_math.cos(angle),_math.sin(angle),
-_math.sin(angle),_math.cos(angle),0.,0.))
rotinv = rot.inv()
invert = _Transformation((-1.,0.,0.,1.,0.,0.))
self._transform = trans*(rotinv*(invert*(rot*(trans.inv()*self._transform))))
self._update({'transformation': self._transform})
def shear(self, shear, angle=0):
"""Shear the object relative to its current reference point.
By default, points with the same y-coordinate as the reference point are left
unchanged. A point d units above the reference point is shifted d * shear
units to the right. The optional angle parameter rotates the axis
that the shearing occurs along.
angle clockwise angle for shear
"""
if not isinstance(shear, (int,float)):
raise TypeError('shear factor must be numeric')
if not isinstance(angle, (int,float)):
raise TypeError('angle must be numeric')
angle = _math.pi*angle/180.
p = self._localToGlobal(self._reference)
trans = _Transformation((1.,0.,0.,1.)+p.get())
rot = _Transformation((_math.cos(angle),_math.sin(angle),
-_math.sin(angle),_math.cos(angle),0.,0.))
rotinv = rot.inv()
sh = _Transformation((1.,-shear,0.,1.,0.,0.))
self._transform = trans*(rotinv*(sh*(rot*(trans.inv()*self._transform))))
self._update({'transformation': self._transform})
def getReferencePoint(self):
"""Return a copy of the current reference point.
Note that mutating that copy has no effect on the Drawable object.
"""
return self._localToGlobal(self._reference)
def adjustReference(self, dx, dy):
"""Move the local reference point relative to its current position.
Note that the object is not moved at all.
"""
if not isinstance(dx, (int,float)):
raise TypeError('dx must be numeric')
if not isinstance(dy, (int,float)):
raise TypeError('dy must be numeric')
p = self._localToGlobal(self._reference)
p = Point(p.getX()+dx, p.getY()+dy)
self._reference = self._globalToLocal(p)
def setDepth(self, depth):
"""Set the depth of the object.
Objects with a higher depth will be rendered behind those with lower depths.
"""
if not isinstance(depth, (int,float)):
raise TypeError('depth must be numeric')
self._depth = depth
self._update({'depth': self._depth})
def getDepth(self):
"""Return the depth of the object."""
return self._depth
def clone(self):
"""Return a duplicate of the drawable object.
The duplicate will have the same properties as the original,
including the sharing of color instances, but the new instance
is not automatically added to those canvases or layers
containing the original.
"""
return _copy.deepcopy(self)
def _localToGlobal(self, point):
if not isinstance(point, Point):
raise TypeError('parameter must be a Point instance')
return self._transform.image(point)
def _globalToLocal(self, point):
if not isinstance(point, Point):
raise TypeError('parameter must be a Point instance')
return self._transform.inv().image(point)
def _beginDraw(self):
"""Deprecated"""
pass
def _completeDraw(self):
"""Deprecated"""
pass
def _objectChanged(self):
"""Deprecated"""
raise NotImplementedError('Deprecated. Please see documentation for _contentsChanged()')
def _draw(self):
"""Cause the object to be drawn (typically, the method is not called directly)."""
raise NotImplementedError('_draw() method must be implemented for each Drawable')
def _contentsChanged(self):
"""Designates that the composition of a (user-defined) Drawable may have changed.
This should be called if an action has taken place that may
effect the composition of _draw for this object, either
because components have been re-ordered, or because components
should be added or replaced.
"""
cacheParent = _graphicsManager._drawParent
cacheChildren = _graphicsManager._drawChildren
_graphicsManager._drawParent = (self, self.__class__) # hopefully this is the correct class
_graphicsManager._drawChildren = []
# important that we call _internalDraw, not _draw
self._internalDraw()
_graphicsManager._frontHierarchy.reviseChildren(self, _graphicsManager._drawChildren)
_graphicsManager._drawParent = cacheParent
_graphicsManager._drawChildren = cacheChildren
def _update(self, properties):
if self in _graphicsManager._frontHierarchy:
_graphicsManager.beginRefresh()
_graphicsManager.addCommandToQueue(('update', self, properties))
_graphicsManager.completeRefresh()
def _getProperties(self):
return {'transformation': self._transform, 'depth': self._depth, 'frozen' : self._frozen}
class Shape(Drawable):
"""A drawable objects that has a border."""
def __init__(self, reference=None):
"""Construct a Shape instance.
reference the initial placement of the shape's reference point.
(default Point(0,0) )
"""
if reference is not None and not isinstance(reference, Point):
raise TypeError('reference point must be a Point instance')
Drawable.__init__(self, reference)
self._borderColor = Color('Black')
self._borderColor._register(self, 'border color')
self._borderWidth = 1
self._dash = (1,0) # solid line
def __deepcopy__(self, memo={}):
temp = Drawable.__deepcopy__(self, memo)
temp._borderColor = self._borderColor # do shallow copy
temp._borderColor._register(temp, 'border color')
return temp
def setBorderColor(self, color):
"""
Set the border color to a copy of the indicated color.
The parameter can be either:
- a string with the name of the color
- an (r,g,b) tuple
- an existing Color instance
"""
if self._borderColor is not color:
old = self._borderColor
if isinstance(color, Color):
self._borderColor = color
else:
try:
self._borderColor = Color(color)
except (TypeError, ValueError):
raise
old._unregister(self, 'border color')
self._borderColor._register(self, 'border color')
self._update({'border color' : self._borderColor})
def getBorderColor(self):
"""Return the color of the object's border."""
return self._borderColor
def setBorderWidth(self, width):
"""Set the width of the border to the indicated width."""
if not isinstance(width, (int,float)):
raise TypeError('border width must be non-negative number')
if width < 0:
raise ValueError('border width cannot be negative')
self._borderWidth = width / self._transform.scale()
self._update({'border width': self._borderWidth})
def getBorderWidth(self):
"""Return the width of the border."""
return self._borderWidth * self._transform.scale()
def setBorderDash(self, dashLength, gapLength=None):
"""Set the border to be a dashed line.
downLength the length of a dash
gapLength the length of interdash space (Default: downLength)
For example,
setBorderDash(3) gives pattern: xxx xxx xxx
setBorderDash(4,1) gives pattern: xxxx xxxx xxxx
setBorderDash(1,4) gives pattern: x x x
Note: gapLength of zero turns this into solid border.
Note: some systems do not properly support dashes with borderWidth greater than 1.
"""
if not isinstance(dashLength, (int,float)):
raise TypeError('dash Length must be numeric')
if dashLength <= 0:
raise ValueError('dash Length must be positive')
if gapLength is None:
gapLength = dashLength
if not isinstance(gapLength, (int,float)):
raise TypeError('space Length must be numeric')
if gapLength < 0:
raise ValueError('space Length must be non-negative')
self._dash = (dashLength, gapLength)
self._update({'dash' : self._dash})
def _getProperties(self):
prop = Drawable._getProperties(self)
prop.update({'border width' : self._borderWidth, 'border color' : Color(self._borderColor),
'dash' : self._dash})
return prop
# putting this at Shape rather than Drawable to avoid stubbing user-defined drawables
def _draw(self): pass
class FillableShape(Shape):
"""A shape that can be filled with an interior color."""
def __init__(self, reference=None):
"""Construct a new FillableShape instance.
The interior color defaults to 'Transparent'.
reference the initial placement of the shape's reference point.
(default Point(0,0) )
"""
if reference is not None and not isinstance(reference, Point):
raise TypeError('reference point must be a Point instance')
Shape.__init__(self, reference)
self._fillColor = Color('Transparent')
self._fillColor._register(self, 'fill color')
def __deepcopy__(self, memo={}):
temp = Shape.__deepcopy__(self, memo)
temp._fillColor = self._fillColor # do shallow copy
temp._fillColor._register(temp, 'fill color')
return temp
def setFillColor(self, color):
"""Set the interior color of the shape to the color.
The parameter can be either:
- a string with the name of the color
- an (r,g,b) tuple
- an existing Color instance
"""
if self._fillColor is not color:
old = self._fillColor
if isinstance(color, Color):
self._fillColor = color
else:
try:
self._fillColor = Color(color)
except (TypeError, ValueError):
raise
old._unregister(self, 'fill color')
self._fillColor._register(self, 'fill color')
self._update({'fill color': self._fillColor})
def getFillColor(self):
"""Return the color of the shape's interior."""
return self._fillColor
def _getProperties(self):
prop = Shape._getProperties(self)
prop['fill color'] = Color(self._fillColor)
return prop
# Canvas class
class Canvas(_GraphicsContainer, _EventTrigger):
"""A window that can be drawn upon."""
def __init__(self, w=200, h=200, bgColor=None, title='Graphics canvas', autoRefresh=True):
"""Create a new drawing canvas.
A new canvas will be created.
w width of drawing area (default 200)
h height of drawing area (default 200)
bgColor color of the background (default 'White')
title window title (default 'Graphics Canvas')
autoRefresh whether auto-refresh mode is used (default True)
"""
_GraphicsContainer.__init__(self)
_EventTrigger.__init__(self)
if not bgColor:
bgColor = 'white'
if not isinstance(w, (int,float)):
raise TypeError('width must be numeric')
if not isinstance(h, (int,float)):
raise TypeError('height must be numeric')
if not isinstance(title, basestring):
raise TypeError('title must be a string')
if not isinstance(autoRefresh, bool):
raise TypeError('autoRefresh flag must be a boolean value')
if isinstance(bgColor, Color):
self._backgroundColor = bgColor
else:
try:
self._backgroundColor = Color(bgColor)
except (TypeError,ValueError):
raise
if Color(self._backgroundColor) == Color('transparent'):
raise ValueError('canvas background cannot be transparent')
self._backgroundColor._register(self, 'background color')
if not _mathMode:
self._transform = _Transformation()
else:
self._transform = _Transformation((1,0,0,-1,0,h))
self._width = w
self._height = h
self._title = title
self._canvasOpen = True
self._mouseCoordinates = Point(0,0)
self._animation = None
self._frozen = False # want initial rendering with title/size/color even if not autoRefresh
self._reference = Point() # TODO: hack because of use in getting event coordinates
_graphicsManager._openCanvases.append(self)
_graphicsManager._frontHierarchy.newCanvas(self)
_graphicsManager.beginRefresh()
_graphicsManager.addCommandToQueue(('create canvas', self, self._getProperties()))
_graphicsManager.completeRefresh()
if not autoRefresh: # turn off auto-refresh before continuing
self.setAutoRefresh(False)
# TODO: get rid of this. temporary hack for 3.0 issue and comparing chains
def __lt__(self, other):
return id(self) < id(other)
def _update(self, properties):
_graphicsManager.beginRefresh()
_graphicsManager.addCommandToQueue(('update', self, properties))
_graphicsManager.completeRefresh()
def _getProperties(self):
# Note: using depth of (0,id(self)) to ensure uniqueness among canvases
return { 'width': self._width, 'height': self._height, 'background color': Color(self._backgroundColor),
'title': self._title, 'transformation': self._transform, 'depth': (0,id(self)),
'frozen' : self._frozen }
def getAutoRefresh(self):
"""Queries current state of the auto-refresh mode.
Returns True if auto-refresh is currently set; False otherwise.
"""
return not self._frozen
def refresh(self):
if self._frozen: # otherwise irrelevant
# force a flush and then re-freeeze
self.setAutoRefresh(True)
self.setAutoRefresh(False)
def setAutoRefresh(self, autoRefresh=True):
"""Change the auto-refresh mode.
When True (the default), every change to the canvas or to an
object drawn upon the canvas will be immediately rendered to
the screen.
When False, all changes are recorded internally, yet not shown
on the screen until the next subsequent call to the refresh()
method of this canvas. This allows multiple changes to be
buffered and rendered all at once.
"""
if not isinstance(autoRefresh, bool):
raise TypeError('autoRefresh flag should be a bool')
if autoRefresh == self._frozen: # if autoRefresh != self.getAutoRefresh()
self._frozen = not autoRefresh
cmd = 'unfreeze' if autoRefresh else 'freeze'
_graphicsManager.beginRefresh()
_graphicsManager.addCommandToQueue((cmd, self))
_graphicsManager.completeRefresh()
def setBackgroundColor(self, color):
"""Set the background color.
The parameter can be either:
- a string with the name of the color
- an (r,g,b) tuple
- an existing Color instance
"""
if self._backgroundColor is not color:
oldColor = self._backgroundColor
if Color(color) == Color('transparent'):
raise ValueError('canvas background cannot be transparent')
if isinstance(color, Color):
self._backgroundColor = color
else:
try:
self._backgroundColor = Color(color)
except (TypeError, ValueError):
raise
oldColor._unregister(self, 'background color')
self._backgroundColor._register(self, 'background color')
self._update({'background color' : Color(self._backgroundColor)})
def getBackgroundColor(self):
"""Return the background color as a Color instance."""
return self._backgroundColor
def setWidth(self, w):
"""Reset the canvas width to w."""
if not isinstance(w, (int,float)):
raise TypeError('width must be numeric value')
if w <= 0:
raise ValueError('width must be positive')
self._width = w
self._update( {'width' : w } )
def getWidth(self):
"""Return the width of the canvas."""
return self._width
def setHeight(self, h):
"""Reset the canvas height to h."""
if not isinstance(h, (int,float)):
raise TypeError('height must be numeric value')
if h <= 0:
raise ValueError('height must be positive')
if _mathMode:
delta = self._height - h
self._height = h
self._transform = self._transform * _Transformation( (1,0,0,1,0,delta) )
self._update( {'height' : h , 'transformation' : self._transform} )
else:
self._height = h
self._update( {'height' : h } )
def getHeight(self):
"""Return the height of the canvas."""
return self._height
def setTitle(self, title):
"""Set the title for the canvas window to given string."""
if not isinstance(title, basestring):
raise TypeError('title must be a string')
self._title = title
self._update( {'title' : title } )
def getTitle(self):
"""Return the title of the window."""
return self._title
def open(self):
"""Opens a graphic window (if not already open).
The window can be closed with a subsequent call to close().
"""
if not self._canvasOpen:
self._update( {'visible' : True } )
self._canvasOpen = True
_graphicsManager._openCanvases.append(self)
def close(self):
"""Close the canvas window (if not already closed).
The window can be reopened with a subsequent call to open().
"""
if self._canvasOpen:
self._update( {'visible' : False } )
self._canvasOpen = False
_graphicsManager._openCanvases.remove(self)
def add(self, drawable):
"""Add the Drawable object to the canvas."""
if not isinstance(drawable, Drawable):
raise TypeError('only Drawable objects can be added to a Canvas')
if drawable in self._contents:
raise ValueError('object already on the Canvas')
if '_transform' not in vars(drawable):
raise Exception('Drawable instance not properly initialized (was parent constructor called?)')
try:
drawable._draw
except AttributeError:
raise Exception('child class of Drawable must provide a _draw method')
if _debug >= 1: print('
Call to Canvas.add with self='+str(self)+' drawable='+str(drawable))
_GraphicsContainer.add(self, drawable)
def remove(self, drawable):
"""Remove the drawable object from the canvas."""
if drawable not in self._contents:
raise ValueError('Object not currently on the Canvas')
_GraphicsContainer.remove(self,drawable)
def setView(self, lowerLeft, upperRight):
"""Set the coordinates for the lower-left corner and upper-right corners of the canvas.
lowerLeft and upperRight are Point instances storing the coordinates of the corners.
"""
if not isinstance(lowerLeft, Point) or not isinstance(upperRight, Point):
raise TypeError('lowerLeft and upperRight must be Point instances')
if lowerLeft.getX() == upperRight.getX() or lowerLeft.getY() == upperRight.getY():
raise ValueError('Lower left and upper right corners must have different x and y coordinates.')
xScale = float(self.getWidth())/(upperRight.getX()-lowerLeft.getX())
yScale = -float(self.getHeight())/(upperRight.getY()-lowerLeft.getY())
xTrans = -xScale*lowerLeft.getX()
yTrans = self.getHeight() - yScale*lowerLeft.getY()
self._transform = _Transformation( (xScale,0,0,yScale,xTrans,yTrans) )
self._update( {'transformation' : self._transform} )
def zoomView(self, factor, fixedPoint=None):
"""Scales the coordinate system for the canvas about the given fixed point.
factor multiplicative zoom factor (must be positive number)
fixedPoint the fixed point for the zoom in local coordinates
(default center of current view)
"""
if not isinstance(factor, (int,float)):
raise TypeError('zoom factor must be a positive number')
if factor <= 0:
raise ValueError('zoom factor must be a positive number')
if fixedPoint is not None:
if not isinstance(fixedPoint, Point):
raise TypeError('fixedPoint must be specified as a Point instance')
else:
fixedPoint = self._transform.inv().image(Point(self.getWidth()/2., self.getHeight()/2.))
self._transform = self._transform * _Transformation( (factor,0,0,factor,
fixedPoint.getX() * (1-factor), fixedPoint.getY()*(1-factor)))
self._update( {'transformation' : self._transform} )
def rotateView(self, angle, fixedPoint=None):
"""Rotates the coordinate system of the canvas about the given fixed point.
angle number of degrees of clockwise rotation
fixedPoint the fixed point for the rotation in local coordinates
(default center of current view)
"""
if not isinstance(angle, (int,float)):
raise TypeError('angle must be numeric')
if fixedPoint is None:
fixedPoint = self._transform.inv().image(Point(self.getWidth()/2., self.getHeight()/2.))
if not isinstance(fixedPoint, Point):
raise TypeError('fixedPoint must be specified as a Point instance')
if not isinstance(fixedPoint, Point):
raise TypeError('fixedPoint must be specified as a Point instance')
translation = _Transformation( (1,0,0,1,fixedPoint.getX(),fixedPoint.getY()) )
angle = -_math.pi*angle/180.
rot = _Transformation((_math.cos(angle),_math.sin(angle),
-_math.sin(angle),_math.cos(angle),0.,0.))
self._transform = self._transform * translation * rot * translation.inv()
self._update( {'transformation' : self._transform} )
def translateView(self, lowerLeft):
"""Translates the viewable portion of the canvas's coordinate system.
lowerLeft the Point in the coordinate system that should be aligned with the
lower-left corner of the Canvas window.
"""
if not isinstance(lowerLeft, Point):
raise TypeError('lowerLeft must be specified as a Point instance')
delta = self._transform.inv().image(Point(0,self.getHeight())) + (-1)*lowerLeft
translation = _Transformation( (1,0,0,1,delta.getX(),delta.getY()) )
self._transform = self._transform * translation
self._update( {'transformation' : self._transform} )
def saveToFile(self, filename):
"""Save a picture of the current canvas to a file.
The filename extension must be a supported file type.
The standard extentions are either .eps or .ps.
If the Python Imaging Library is installed then addition
supported file types are: .gif, .jpg, .jpeg, .png
"""
if not isinstance(filename, str):
raise TypeError('filename must be a string')
if '.' not in filename:
raise ValueError('filename extension should indicate file type')
ext = filename.split('.')[-1].lower()
if not _pilAvailable:
choices = ('eps', 'ps')
else:
choices = ('eps', 'ps', 'gif', 'jpg', 'jpeg', 'png')
if ext not in choices:
raise ValueError('Unsupported file type. Choices: ' + ' '.join(choices))
if ext in ('eps','ps'):
epsFilename = filename
else:
fd, epsFilename = _tempfile.mkstemp('.eps')
_os.close(fd)
_graphicsManager.executeFunction( ('save to file', self, epsFilename,
self.getBackgroundColor()) )
if ext not in ('eps','ps'): # Use PIL to convert
image = _Image.open(epsFilename).convert('RGBA')
image.save(filename)
_os.remove(epsFilename)
def getMouseCoordinates(self):
"""Return the current coordinate of the mouse."""
return self._mouseCoordinates
class _RenderedCanvas(object):
def __init__(self, chain, properties):
if _debug >= 1: print('Creating _RenderedCanvas')
self._parent = chain[-1][0]
self._tkWin = _Tkinter.Toplevel()
self._tkWin.protocol('WM_DELETE_WINDOW', self._parent.close)
self._tkWin.title(properties['title'])
self._w = properties['width']
self._h = properties['height']
self._canvas = _Tkinter.Canvas(self._tkWin, width=self._w, height=self._h,
highlightthickness=0,
background=Color._getTkColor(properties['background color']))
self._canvas.pack(expand=False, side=_Tkinter.TOP)
self._tkWin.resizable(0,0)
# Setup function to deal with events
callback = lambda event : self._handleEvent(event)
self._canvas.bind('<Button>', callback)
self._canvas.bind('<ButtonRelease>', callback)
self._canvas.bind('<Key>', callback)
self._canvas.bind('<Motion>', callback)
self._canvas.bind('<Enter>', callback)
self._canvas.focus_set()
def update(self, properties):
if 'title' in properties:
self._tkWin.title(properties['title'])
if 'width' in properties:
self._w = properties['width']
self._canvas.config(width=self._w)
if 'height' in properties:
self._h = properties['height']
self._canvas.config(height=self._h)
if 'background color' in properties:
self._canvas.config(background=Color._getTkColor(properties['background color']))
if 'visible' in properties:
if not properties['visible']:
self._tkWin.withdraw()
else:
self._tkWin.deiconify()
def saveToFile(self, filename, bgcolor):
# add rectangle to simulate background color
fakeBG = self._canvas.create_polygon((0,0), (self._w,0), (self._w,self._h), (0,self._h),
fill=Color._getTkColor(bgcolor),outline='')
self._canvas.lower(fakeBG)
try:
self._canvas.postscript(file=filename)
except KeyboardInterrupt:
raise
except:
pass
self._canvas.delete(fakeBG)
def _handleEvent(self, event):
# Create the event
e = Event()
if not _graphicsManager._mousePrevPosition:
e._prevx, e._prevy = event.x, event.y
else:
e._prevx, e._prevy = _graphicsManager._mousePrevPosition[0], _graphicsManager._mousePrevPosition[1]
_graphicsManager._mousePrevPosition = (int(event.x), int(event.y))
e._x, e._y = event.x, event.y
# Set the mouse coordinates
# TODO must deal with tranformations for top level on all coordinates
self._parent._mouseCoordinates = Point(e._x, e._y)
if int(event.type) == 2: # Keypress
e._eventType = 'keyboard'
if event.char:
e._key = event.char
else:
if event.keysym == 'Return':
e._key = '
'
elif event.keysym == 'BackSpace':
e._key = ''
elif event.keysym == 'Tab':
e._key = ' '
else:
return # ignore this event.
elif int(event.type) == 4: # Mouse click
e._eventType = 'mouse click'
e._button = event.num
_graphicsManager._mouseButtonDown = True
elif int(event.type) == 5: # Mouse release
e._eventType = 'mouse release'
e._button = event.num
_graphicsManager._mouseButtonDown = False
elif int(event.type) == 6: # Mouse move
self._canvas._mouseCoordinates = Point(e._x, e._y)
if _graphicsManager._mouseButtonDown:
e._eventType = 'mouse drag'
else:
return
else:
return
# Find the shape where the event occurred:
tkIds = self._canvas.find_overlapping(event.x, event.y, event.x, event.y)
if len(tkIds) > 0:
chain = _graphicsManager._objectIdRegistry[(self._canvas, tkIds[-1])]._chain
else:
chain = ((self._parent,Canvas),)
for i in range(len(chain),0,-1):
subchain = chain[:i]
e._trigger = subchain[-1][0]
for h in _graphicsManager._eventHandlers.get(e._trigger,set()):
transformedEvent = _copy.copy(e)
cumInv = _graphicsManager._renderedHierarchy.getNode(subchain)._cumulativeTransformation.inv()
local = _graphicsManager._renderedHierarchy.getNode(subchain)._transformation
trans = local.image(e._trigger._reference) # TODO make property; not thread safe
p = local.image(cumInv.image(Point(e._x, e._y)))
transformedEvent._x = p._x - trans._x
transformedEvent._y = p._y - trans._y
_graphicsManager.addEventToQueue(h, transformedEvent)
# Layer class
class Layer(Drawable, _GraphicsContainer):
"""A composite that represents a group of shapes as a single drawable object.
Objects are placed onto the layer relative to the coordinate
system of the layer itself. The layer can then be placed onto a
canvas (or even onto another layer).
"""
def __init__(self):
"""Construct a new Layer instance.
The layer is initially empty.
The reference point of that layer is initially the origin in
its own coordinate system, (0,0).
"""
Drawable.__init__(self)
_GraphicsContainer.__init__(self)
self._final = False
def finalize(self):
"""Finalize the layer.
Once finalized, objects can no longer be added or deleted.
"""
self._final = True
def add(self, drawable):
"""Add the Drawable object to the layer."""
if _debug >= 1: print('
Call to Layer.add with self='+str(self)+' drawable='+str(drawable))
if self._final:
raise Exception('cannot add objects once finalized')
if not isinstance(drawable, Drawable):
raise TypeError('parameter must be an instance of a Drawable object')
if drawable in self._contents:
raise ValueError('object is already on the Layer')
if '_transform' not in vars(drawable):
raise Exception('Drawable not properly initialized (was parent constructor called?)')
try:
drawable._draw
except KeyboardInterrupt:
raise
except:
raise Exception('Drawable class must have a _draw method')
_GraphicsContainer.add(self, drawable)
def remove(self, drawable):
"""Remove the Drawable object from the layer.
A ValueError is raised if the drawable is not currently in the layer.
"""
if self._final:
raise Exception('cannot remove objects once finalized')
if drawable not in self._contents:
raise ValueError('object not currently on the Layer')
_GraphicsContainer.remove(self,drawable)
def clear(self):
"""Remove all objects from the layer."""
if self._final:
raise Exception('cannot remove objects once finalized')
_GraphicsContainer.clear(self)
def _draw(self):
for shape in self._contents: # according to inserted order
shape._draw()
class Circle(FillableShape):
"""A circle that can be drawn to a canvas."""
def __init__(self, radius=10, centerPt=None):
"""Construct a new instance of Circle.
radius the circle's radius (default 10)
centerPt a Point representing the placement of the circle's center
(default Point(0,0) )
The reference point for a circle is originally its center.
"""
if not isinstance(radius, (int,float)):
raise TypeError('radius must be numeric')
if radius <= 0:
raise ValueError("radius must be positive")
if centerPt and not isinstance(centerPt, Point):
raise TypeError("circle's center must be specified as a Point")
FillableShape.__init__(self)
if not centerPt:
centerPt = Point()
oldBorderWidth = self.getBorderWidth()
self._transform = _Transformation( (radius,0.,0.,radius,centerPt.getX(),centerPt.getY()) )
self._borderWidth = oldBorderWidth / self._transform.scale()
def setRadius(self, r):
"""Set the radius of the circle to r."""
if not isinstance(r, (int,float)):
raise TypeError('radius must be numeric')
if r <= 0:
raise ValueError("radius must be positive")
factor = float(r)/self.getRadius()
oldBorderWidth = self.getBorderWidth()
self._transform = self._transform * _Transformation((factor,0.,0.,factor,0.,0.))
self._borderWidth = oldBorderWidth / self._transform.scale()
self._update({'transformation': self._transform, 'border width': self._borderWidth})
def getRadius(self):
"""Return the radius of the circle."""
return _math.sqrt(self._transform._matrix[0]**2 + self._transform._matrix[1]**2)
class Ellipse(FillableShape):
"""A ellipse that can be drawn to a canvas."""
def __init__(self, w=10, h=10, centerPt=None):
"""Construct a new instance of Circle.
w the ellipse's width (default 10)
h the ellipse's height (default 10)
centerPt a Point representing the placement of the circle's center
(default Point(0,0) )
The reference point for a ellipse is originally its center.
"""
if not isinstance(w, (int,float)):
raise TypeError('width must be numeric')
if w <= 0:
raise ValueError('width must be positive')
if not isinstance(h, (int,float)):
raise TypeError('height must be numeric')
if h <= 0:
raise ValueError('height must be positive')
if centerPt and not isinstance(centerPt, Point):
raise TypeError("center must be specified as a Point")
FillableShape.__init__(self) # intentionally not sending center
if not centerPt:
centerPt = Point()
oldBorderWidth = self.getBorderWidth()
self._transform = _Transformation( (.5*w, 0., 0., .5*h, centerPt.getX(), centerPt.getY()) )
self._borderWidth = oldBorderWidth / self._transform.scale()
def getWidth(self):
"""Return the width of the ellipse."""
return 2*_math.sqrt(self._transform._matrix[0]**2 + self._transform._matrix[2]**2)
def getHeight(self):
"""Return the height of the ellipse."""
return 2*_math.sqrt(self._transform._matrix[1]**2 + self._transform._matrix[3]**2)
def setWidth(self, w):
"""Set the width of the ellipse to w."""
if not isinstance(w, (int,float)):
raise TypeError('width must be numeric')
if w <= 0:
raise ValueError("width must be positive")
factor = float(w)/self.getWidth()
oldBorderWidth = self.getBorderWidth()
self._transform = self._transform * _Transformation((factor,0.,0.,1.,0.,0.))
self._borderWidth = oldBorderWidth / self._transform.scale()
self._update({'transformation': self._transform, 'border width': self._borderWidth})
def setHeight(self, h):
"""Set the height of the ellipse to h."""
if not isinstance(h, (int,float)):
raise TypeError('height must be numeric')
if h <= 0:
raise ValueError("height must be numeric")
factor = float(h)/self.getHeight()
oldBorderWidth = self.getBorderWidth()
self._transform = self._transform * _Transformation((1.,0.,0.,factor,0.,0.))
self._borderWidth = oldBorderWidth / self._transform.scale()
self._update({'transformation': self._transform, 'border width': self._borderWidth})
class Rectangle(FillableShape):
"""A rectangle that can be drawn to the canvas."""
def __init__(self, w=20, h=10, centerPt=None):
"""
Construct a new instance of a Rectangle.
The reference point for a rectangle is its center.
w the width of the rectangle (default 20)
h the height of the rectangle (default 10)
centerPt a Point representing the placement of the rectangle's center
(default Point(0,0) )
"""
if not isinstance(w, (int,float)):
raise TypeError('width must be numericr')
if w <= 0:
raise ValueError('width must be positive')
if not isinstance(h, (int,float)):
raise TypeError('height must be numeric')
if h <= 0:
raise ValueError('height must be positive')
if centerPt and not isinstance(centerPt, Point):
raise TypeError('center must be specified as a Point')
FillableShape.__init__(self) # intentionally not sending center point
if not centerPt:
centerPt = Point(0,0)
oldBorderWidth = self.getBorderWidth()
self._transform = _Transformation( (w, 0., 0., h, centerPt.getX(), centerPt.getY()) )
self._borderWidth = oldBorderWidth / self._transform.scale()
def getWidth(self):
"""Return the width of the rectangle."""
return _math.sqrt(self._transform._matrix[0]**2 + self._transform._matrix[2]**2)
def getHeight(self):
"""Return the height of the rectangle."""
return _math.sqrt(self._transform._matrix[1]**2 + self._transform._matrix[3]**2)
def setWidth(self, w):
"""Set the width of the rectangle to w."""
if not isinstance(w, (int,float)):
raise TypeError('width must be a positive number')
if w <= 0:
raise ValueError("width must be positive")
factor = float(w) / self.getWidth()
oldBorderWidth = self.getBorderWidth()
p = self._localToGlobal(self._reference)
trans = _Transformation((1.,0.,0.,1.)+p.get())
sca = _Transformation((factor,0.,0.,1.,0.,0.))
self._transform = trans*(sca*(trans.inv()*self._transform))
self._borderWidth = oldBorderWidth / self._transform.scale()
self._update({'transformation': self._transform, 'border width': self._borderWidth})
def setHeight(self, h):
"""Set the height of the rectangle to h."""
if not isinstance(h, (int,float)):
raise TypeError('height must be a positive number')
if h <= 0:
raise ValueError("height must be positive")
factor = float(h) / self.getHeight()
oldBorderWidth = self.getBorderWidth()
p = self._localToGlobal(self._reference)
trans = _Transformation((1.,0.,0.,1.)+p.get())
sca = _Transformation((1.,0.,0.,factor,0.,0.))
self._transform = trans*(sca*(trans.inv()*self._transform))
self._borderWidth = oldBorderWidth / self._transform.scale()
self._update({'transformation': self._transform, 'border width': self._borderWidth})
class Square(Rectangle):
"""A square that can be drawn to the canvas."""
def __init__(self, size=10, centerPt=None):
"""
Construct a new Square instance.
The reference point for a square is its center.
size the dimension of the square (default 10)
centerPt a Point representing the placement of the rectangle's center
(defaults Point(0,0) )
"""
if not isinstance(size, (int,float)):
raise TypeError('size must be numeric')
if size <= 0:
raise ValueError('size must be positive')
if centerPt and not isinstance(centerPt, Point):
raise TypeError('center must be specified as a Point')
Rectangle.__init__(self, size, size, centerPt)
def getSize(self):
"""Return the length of a side of the square."""
return self.getWidth()
def setSize(self, s):
"""Set the width and height of the square to s."""
if not isinstance(s, (int,float)):
raise TypeError('size must be numeric')
if s <= 0:
raise ValueError('size must be positive')
# TODO: Could do freeze/unfreeze to make atomic (if not currently frozen)
Rectangle.setWidth(self, s)
Rectangle.setHeight(self, s)
def setWidth(self, w):
"""Set the width and height of the square to w."""
if not isinstance(w, (int,float)):
raise TypeError('width must be numeric')
if w <= 0:
raise ValueError("width must be positive")
self.setSize(w)
def setHeight(self, h):
"""Set the width and height of the square to h."""
if not isinstance(h, (int,float)):
raise TypeError('height must be numeric')
if h <= 0:
raise ValueError("height must be positive")
self.setSize(h)
class Path(Shape):
"""A path that can be drawn to a canvas."""
def __init__(self, *points):
"""Construct a new instance of a Path.
The path is described as a series of points that are connected in order.
These points can be initialized by sending each individual Point
as a separate parameter, or by sending a single parameter
containing a sequence of Points. If no parameters are sent, the
path initially has zero points.
The reference point for a path is initially aligned with the first
point of the path.
"""
Shape.__init__(self)
if len(points) == 1:
try:
points = tuple(points[0])
except TypeError:
pass # original parameter might be a single Point
for p in points:
if not isinstance(p, Point):
raise TypeError('non-Point specified as parameter')
self._points = list(points)
if len(self._points) >= 1:
self.adjustReference(self._points[0].getX(), self._points[0].getY())
self._final = False
self._arrows = (False,False)
def _getProperties(self):
prop = Shape._getProperties(self)
prop['points'] = tuple(self._points)
prop['arrows'] = self._arrows
return prop
def finalize(self):
"""Finalize the shape.
Once finalized, points can no longer be added, deleted, or modified.
"""
self._final = True
def addPoint(self, point, index=-1):
"""Add a new point to the Path.
point a Point instance
index designates where on the path the new point is placed
(default at the end)
"""
if self._final:
raise Exception('cannot add points once finalized')
if not isinstance(point, Point):
raise TypeError('parameter must be a Point instance')
if index > -1:
self._points.insert(index, point)
else:
self._points.append(point)
if len(self._points) == 1: # first point added
self._reference = Point(point.getX(), point.getY())
self._update({'points': tuple(self._points)})
def deletePoint(self, index=-1):
"""Remove the Point at the given index.
By default, deletes the last point.
"""
if self._final:
raise Exception('cannot delete points once finalized')
if not isinstance(index, int):
raise TypeError('index must be an integer')
try:
self._points.pop(index)
except IndexError:
raise IndexError('index out of range')
self._update({'points': tuple(self._points)})
def clearPoints(self):
"""Remove all points, setting this back to an empty Path."""
if self._final:
raise Exception('cannot clear points once finalized')
self._points = list()
self._update({'points': tuple(self._points)})
def getNumberOfPoints(self):
"""Return the current number of points."""
return len(self._points)
def getPoint(self, index):
"""Return a copy of the Point at the given index.
Subsequently mutating that copy has no effect on the Path.
"""
if not isinstance(index, int):
raise TypeError('index must be an integer')
try:
p = self._points[index]
except IndexError:
raise IndexError('index out of range')
return Point(p.getX(), p.getY())
def setPoint(self, point, index=-1):
"""Change the Point at the given index to a new value.
By default, the last point is changed.
"""
if self._final:
raise Exception('cannot modify points once finalized')
if not isinstance(index, int):
raise TypeError('index must be an integer')
if not isinstance(point, Point):
raise TypeError('first parameter must be a Point instance')
try:
self._points[index] = point
except IndexError:
raise IndexError('index out of range')
self._update({'points': tuple(self._points)})
def getPoints(self):
"""Return a list of Point instances that are copies of the points on the Path."""
return list(self._points)
def setArrows(self, forward, reverse=False):
"""Change setting for whether arrows are drawn at beginning and end of path.
If forward is True, will draw an arrow at the last point on the path.
Otherwise, no such arrow is drawn.
If reverse is True, will draw a reverse arrow eminating from
the first point on the path; otherwise (the default), no such
arrow is drawn.
Note: arrows are never displayed for Polygon or ClosedSpline instances
"""
self._arrows = (forward,reverse)
self._update({'arrows' : self._arrows})
class Polygon(Path,FillableShape):
"""A polygon that can be drawn to a canvas."""
def __init__(self, *points):
"""Construct a new Polygon instance.
The polygon is described as a series of points that are connected in order.
The last point is automatically connected back to the first to close the polygon.
These points can be initialized by sending each individual Point
as a separate parameter, or by sending a single parameter
containing a sequence of Points. If no parameters are sent, the
polygon initially has zero points.
The reference point for a polygon is initially aligned with the
first point of the polygon.
"""
FillableShape.__init__(self)
try:
Path.__init__(self, *points)
except TypeError:
raise
def _getProperties(self): # need aspects of both parents
prop = Path._getProperties(self)
prop.update(FillableShape._getProperties(self))
return prop
class Spline(Path):
"""A curved path that can be drawn to a canvas."""
def __init__(self, *points):
"""
Construct a new instance of a Spline.
The spline is described as a series of points that are connected in order
with curves.
These points can be initialized by sending each individual Point
as a separate parameter, or by sending a single parameter
containing a sequence of Points. If no parameters are sent, the
path initially has zero points.
The reference point for a spline is initially aligned with the first
point of the spline.
"""
try:
Path.__init__(self, *points)
except TypeError:
raise
def _getProperties(self):
prop = Path._getProperties(self)
prop['smooth'] = True # need key, but value is really irrelevant
return prop
class ClosedSpline(Polygon):
"""A closed curve that can be drawn to a canvas."""
def __init__(self, *points):
"""Construct a new ClosedSpline instance.
The cuved spline is described as a series of points that are connected in order.
The last point is automatically connected back to the first to close the spline.
These points can be initialized by sending each individual Point
as a separate parameter, or by sending a single parameter
containing a sequence of Points. If no parameters are sent, the
polygon initially has zero points.
The reference point for a closed spline is initially aligned with the
first point of the spline.
"""
try:
Polygon.__init__(self, *points)
except TypeError:
raise
def _getProperties(self):
prop = Polygon._getProperties(self)
prop['smooth'] = True # need key, but value is really irrelevant
return prop
class Text(Drawable):
"""A piece of text that can be drawn to a canvas."""
def __init__(self, message='', fontsize=12, centerPt=None):
"""
Construct a new Text instance.
The text color is initially black, although this can be changed by
setColor. The reference point for the text is initially its center.
message a string which is to be displayed (default empty string)
fontsize the font size (default 12)
centerPt where to locate the center of the text (default Point(0,0) )
By default, multiline text will be left-justified, although
this style can be changed by the setJustification method.
"""
if not isinstance(message, basestring):
raise TypeError('message must be a string')
if not isinstance(fontsize, (int,float)):
raise TypeError('fontsize must be numeric')
if fontsize <= 0:
raise ValueError('fontsize must be positive')
if centerPt and not isinstance(centerPt, Point):
raise TypeError('center must be a Point')
Drawable.__init__(self)
self._text = message
self._size = fontsize
self._color = Color('black')
self._color._register(self, 'font color')
if centerPt:
self.move(centerPt.getX(), centerPt.getY())
self._justify = 'left'
def __deepcopy__(self, memo={}):
temp = Drawable.__deepcopy__(self, memo)
temp._color = self._color # do shallow copy
temp._color._register(temp, 'font color')
return temp
def _draw(self): pass
def _getProperties(self):
prop = Drawable._getProperties(self)
prop.update( { 'message' : self._text, 'font color' : Color(self._color),
'font size' : self._size, 'justify' : self._justify } )
return prop
def setMessage(self, message):
"""Set the string to be displayed.
message a string
"""
if not isinstance(message, basestring):
raise TypeError('message must be a string')
self._text = message
self._update({'message': message})
def getMessage(self):
"""Return the current string."""
return self._text
def setFontColor(self, color):
"""Set the color of the font.
The parameter can be either:
- a string with the name of the color
- an (r,g,b) tuple
- an existing Color instance
"""
if self._color is not color:
old = self._color
if isinstance(color, Color):
self._color = color
else:
try:
self._color = Color(color)
except (TypeError, ValueError):
raise
old._unregister(self, 'font color')
self._color._register(self, 'font color')
self._update({'font color': Color(self._color)})
def getFontColor(self):
"""Return the current font color."""
return self._color
def setFontSize(self, fontsize):
"""Set the font size."""
if not isinstance(fontsize, (int,float)):
raise TypeError('fontsize must be numeric')
if fontsize <= 0:
raise ValueError('fontsize must be positive')
self._size = fontsize
self._update({'font size': self._size})
def getFontSize(self):
"""Return the current font size."""
return self._size
def scale(self, factor):
"""Scale the object relative to its current reference point.
factor scale is multiplied by this number (must be positive)
"""
if not isinstance(factor, (int,float)):
raise TypeError('scaling factor must be a positive number')
if factor <= 0:
raise ValueError('scaling factor must be a positive number')
Drawable.scale(self, factor) # transform is really irrelevant, but leaving this to support TextBox type usage
self._size *= factor
self._update({'font size': self._size})
def rotate(self,angle):
"""Not yet implemented."""
raise NotImplementedError('rotating text is not yet implemented')
def stretch(self,xFactor,yFactor,angle=0):
"""Not yet implemented."""
raise NotImplementedError('stretching text is not yet implemented')
def flip(self,angle=0):
"""Not yet implemented."""
raise NotImplementedError('fliping text is not yet implemented')
def shear(self, shear, angle=0):
"""Not yet implemented."""
raise NotImplementedError('shearing text is not yet implemented')
def getDimensions(self):
"""Return a (width,height) tuple measuring visual dimensions of currently displayed message."""
return _graphicsManager.executeFunction( ('get text size', self._text, self._size) )
def setJustification(self, style):
"""Set the justifcation style for multiline text.
style must be either 'left', 'right', or 'center'
By default, text is center justified.
"""
if not isinstance(style, basestring):
raise TypeError('style must be a string')
if style not in ('left', 'right', 'center'):
raise ValueError("style must be 'left', 'right', or 'center'")
self._justify = style
self._update({'justify': style})
class Image(Drawable):
"""A wrapper for images that can be drawn to a canvas and manipulated."""
def __init__(self, *args):
"""Construct a new Image instance.
If invoked as Image(filename), the image will be constructed
based on the contents of an underlying image file.
gif format should be supported universally; support for
additional image formats (e.g., jpg) will be system dependent.
Install Pythin Image Library (PIL) for more options.
If invoked as Image(width, height), a new image is created
with the given dimensions, and with all pixels are initially
Transparent.
Once it is constructed, the virtual width and height of the
image is fixed, and a coordinate system is used for accessing
individual pixels of the image.
However, the virtual image may be rendered at any size on a
Canvas through use of methods such as scale inherited from Drawable.
The center of the image is initially aligned with Point(0,0).
"""
Drawable.__init__(self)
if not 1 <= len(args) <= 2:
raise TypeError('must either specify a filename or integer width and height')
if len(args) == 2:
for k in (0,1):
if not isinstance(args[k], int):
msg = ('width','height')[k] + ' must be an integer'
raise TypeError(msg)
if args[k] <= 0:
msg = ('width','height')[k] + ' must be positive'
raise ValueError(msg)
self._w = args[0]
self._h = args[1]
self._data = _array('B', [255]) * (3 * self._w * self._h)
self._alpha = _array('B', [0]) * ((self._w * self._h + 7)// 8) # bitfield (all transparent)
self._image = None
if len(args) == 1:
# TODO: add back in base64 encoded strings for initialization? (from KAIST)
if not isinstance(args[0], basestring):
raise TypeError('filename must be a string')
result = _graphicsManager.executeFunction( ('load image', args[0]) )
if result is None:
raise ValueError('unable to load image file: ' + args[0])
self._image, self._w, self._h = result
self._data = self._alpha = _array('B')
def _draw(self): pass
def _getProperties(self):
prop = Drawable._getProperties(self)
prop.update( { 'width' : self._w, 'height' : self._h, 'image' : self._image,
'data' : self._data[:], 'alpha' : self._alpha[:] } )
return prop
def getWidth(self):
"""Return the number of pixels per row in the original coordinate space."""
return self._w
def getHeight(self):
"""Return the number of pixels per column in the original coordinate space."""
return self._h
def getPixel(self, x, y):
"""Returns a copy of the color at the specified pixel."""
if not isinstance(x, int):
raise TypeError('x must be integral')
if not 0 <= x < self._w:
raise ValueError('x is invalid index')
if not isinstance(y, int):
raise TypeError('y must be integral')
if not 0 <= y < self._h:
raise ValueError('y is invalid index')
if len(self._data) == 0: # lazy conversion
self._data, self._alpha =
_graphicsManager.executeFunction( ('convert image', self._image) )
scalar = x + self._w * y
a,b = divmod(scalar, 8)
if self._alpha[a] & (1 << b):
return Color(tuple(self._data[3*scalar:3*(scalar+1)]))
else:
return Color('transparent')
def setPixel(self, x, y, color):
"""Set the specified pixel to the given color.
The parameter can be either:
- a string with the name of the color
- an (r,g,b) tuple
- an existing Color instance (which will be copied)
Note: Images are intentionally implemented so that individual
calls to setPixel are not immediately rendered. You must call
updatePixels() to force all changes to be rendered.
"""
if not isinstance(x, int):
raise TypeError('x must be integral')
if not 0 <= x < self._w:
raise ValueError('x is invalid index')
if not isinstance(y, int):
raise TypeError('y must be integral')
if not 0 <= y < self._h:
raise ValueError('y is invalid index')
try:
c = Color(color)
except (TypeError, ValueError):
raise
scalar = x + self._w * y
a,b = divmod(scalar, 8)
if len(self._data) == 0: # lazy conversion
self._data, self._alpha =
_graphicsManager.executeFunction( ('convert image', self._image) )
if c == Color('transparent'):
self._alpha[a] &= (255-(1 << b)) # set alpha to zero
else:
rgb = [int(k) for k in c.getColorValue()]
self._data[3*scalar:3*(scalar+1)] = _array('B', rgb)
self._alpha[a] |= (1 << b) # set alpha to one
def updatePixels(self):
"""Re-render the image to reflect current pixel settings."""
self._update({'data': self._data[:], 'alpha' : self._alpha[:]})
# Rendered shapes
class _RenderedDrawable(object):
def __init__(self, chain, properties):
self._chain = chain
self._canvas = _graphicsManager._renderedHierarchy.getNode(chain[:1])._renderedDrawable
self._object = None
def putAbove(self, other):
if other is not None:
self._canvas._canvas.lift(self._object, other._object)
else: # Put at bottom
self._canvas._canvas.lower(self._object)
def update(self, properties):
if _debug >= 1: print('Updating _RenderedDrawable')
pass
def remove(self):
self._canvas._canvas.delete(self._object)
class _RenderedShape(_RenderedDrawable):
def __init__(self, chain, properties):
_RenderedDrawable.__init__(self, chain, properties)
self._width = self._transWidth = self._dash = self._borderColor = None
def update(self, properties):
configs = {} # will eventually send entries to itemconfigure
# deal with silly Tk conventions
if isinstance(self, _RenderedFillableShape):
colorProp = 'outline'
else:
colorProp = 'fill'
if 'border width' in properties or 'transformation' in properties:
# effective width may have changed
w = properties.get('border width', self._width)
if w != self._width:
if w == 0: # changing from nonzero to zero
configs[colorProp] = ''
self._transWidth = 0
elif self._width == 0: # changing from zero to nonzero!
configs[colorProp] = Color._getTkColor(properties.get('border color',self._borderColor))
self._width = w
if w != 0: # recompute transformed width
transform = _graphicsManager._renderedHierarchy.getNode(self._chain)._cumulativeTransformation
self._transWidth = w * transform.scale()
configs['width'] = self._transWidth
if self._dash is not None and self._dash[1] != 0:
a = min(255, max(1, int(round(self._dash[0]*self._transWidth/self._width))))
b = min(255, max(1, int(round(self._dash[1]*self._transWidth/self._width))))
# for some reason, (a,b,a,b) tuple works better than (a,b) tuple for Tkinter
configs['dash'] = (a,b) * _dashMultiplier
if 'border color' in properties:
self._borderColor = properties['border color']
c = Color._getTkColor(self._borderColor)
if self._width != 0: # border is currently rendered
configs[colorProp] = c
if 'dash' in properties and properties['dash'] != self._dash:
self._dash = properties['dash']
if self._dash[1] == 0:
configs['dash'] = ''
else:
a = min(255, max(1, int(round(self._dash[0]*self._transWidth/self._width))))
b = min(255, max(1, int(round(self._dash[1]*self._transWidth/self._width))))
# for some reason, (a,b,a,b) tuple works better than (a,b) tuple for Tkinter
configs['dash'] = (a,b) * _dashMultiplier
self._canvas._canvas.itemconfigure(self._object, **configs)
_RenderedDrawable.update(self, properties)
class _RenderedFillableShape(_RenderedShape):
def __init__(self, chain, properties):
_RenderedShape.__init__(self, chain, properties)
self._fillColor = None
def update(self, properties):
if 'fill color' in properties:
self._fillColor = properties['fill color']
self._canvas._canvas.itemconfigure(self._object, fill=Color._getTkColor(self._fillColor))
_RenderedShape.update(self, properties)
class _RenderedCircle(_RenderedFillableShape):
def __init__(self, chain, properties):
_RenderedFillableShape.__init__(self, chain, properties)
transform = _graphicsManager._renderedHierarchy.getNode(chain)._cumulativeTransformation
points = []
for i in range(0,360,5):
points.append(Point(1,0) ^ i)
statement = 'self._object = self._canvas._canvas.create_polygon('
for p in points:
statement += str(transform.image(p).getX()) + ', ' + str(transform.image(p).getY()) + ', '
statement += 'smooth=1)'
exec(statement)
_graphicsManager._objectIdRegistry[(self._canvas._canvas,self._object)] = self
_RenderedFillableShape.update(self, properties)
def update(self, properties):
if 'transformation' in properties:
transform = _graphicsManager._renderedHierarchy.getNode(self._chain)._cumulativeTransformation
points = []
for i in range(0,360,5):
points.append(Point(1,0) ^ i)
statement = 'self._canvas._canvas.coords(self._object'
for p in points:
statement += ', ' + str(transform.image(p).getX()) + ', ' + str(transform.image(p).getY())
statement += ')'
exec(statement)
_RenderedFillableShape.update(self, properties)
class _RenderedRectangle(_RenderedFillableShape):
def __init__(self, chain, properties):
_RenderedFillableShape.__init__(self, chain, properties)
transform = _graphicsManager._renderedHierarchy.getNode(self._chain)._cumulativeTransformation
points = [Point(-.5,-.5), Point(-.5,.5), Point(.5,.5), Point(.5,-.5)]
for i in range(4):
points[i] = transform.image(points[i])
self._object = self._canvas._canvas.create_polygon(points[0].get(), points[1].get(), points[2].get(), points[3].get())
_graphicsManager._objectIdRegistry[(self._canvas._canvas,self._object)] = self
_RenderedFillableShape.update(self, properties)
def update(self, properties):
if 'transformation' in properties:
transform = _graphicsManager._renderedHierarchy.getNode(self._chain)._cumulativeTransformation
points = [Point(-.5,-.5), Point(-.5,.5), Point(.5,.5), Point(.5,-.5)]
for i in range(4):
points[i] = transform.image(points[i])
self._canvas._canvas.coords(self._object, points[0].getX(), points[0].getY(), points[1].getX(), points[1].getY(),
points[2].getX(), points[2].getY(), points[3].getX(), points[3].getY())
_RenderedFillableShape.update(self, properties)
class _RenderedPath(_RenderedShape):
def __init__(self, chain, properties):
_RenderedShape.__init__(self, chain, properties)
transform = _graphicsManager._renderedHierarchy.getNode(self._chain)._cumulativeTransformation
self._points = properties['points']
if len(self._points) > 1:
tkPts = [(transform.image(p).getX(),transform.image(p).getY()) for p in self._points]
else:
tkPts = [(0,0)] * 3
self._object = self._canvas._canvas.create_line(tkPts)
_graphicsManager._objectIdRegistry[(self._canvas._canvas,self._object)] = self
_RenderedShape.update(self, properties)
configs = {}
if 'smooth' in properties:
configs['smooth'] = 1
if 'arrows' in properties:
transform = _graphicsManager._renderedHierarchy.getNode(self._chain)._cumulativeTransformation
w = transform.scale()*properties['border width']
configs['arrowshape'] = str(w*8) + ' ' + str(w*10) + ' ' + str(w*3)
pair = properties['arrows']
if pair[0] and pair[1]:
configs['arrow'] = 'both'
elif pair[0]:
configs['arrow'] = 'last'
elif pair[1]:
configs['arrow'] = 'first'
if not self._points: # make effectively invisible TK object with width 0
configs.update( {'fill' : None, 'width' : 0} )
if configs:
self._canvas._canvas.itemconfigure(self._object, **configs)
def update(self, properties):
_RenderedShape.update(self, properties)
configs = {}
if 'transformation' in properties or 'points' in properties:
wasEmpty = len(self._points) < 2
self._points = properties.get('points', self._points) # update if given
if len(self._points) > 1:
transform = _graphicsManager._renderedHierarchy.getNode(self._chain)._cumulativeTransformation
tkCoords = []
for p in self._points:
tkCoords.append(transform.image(p).getX())
tkCoords.append(transform.image(p).getY())
tkCoords = tuple(tkCoords)
if wasEmpty: # need to explicitly (re)set border properties
configs['width'] = self._transWidth
configs['fill'] = Color._getTkColor(self._borderColor)
else:
tkCoords = 6 * (0,)
self._canvas._canvas.coords(self._object,tuple(tkCoords))
if 'transformation' in properties or 'border width' in properties:
w = self._transWidth
configs['arrowshape'] = str(w*8) + ' ' + str(w*10) + ' ' + str(w*3)
if 'arrows' in properties:
pair = properties['arrows']
if pair[0] and pair[1]:
configs['arrow'] = 'both'
elif pair[0]:
configs['arrow'] = 'last'
elif pair[1]:
configs['arrow'] = 'first'
else:
configs['arrow'] = 'none'
if not self._points: # make effectively invisible TK object with width 0
configs.update( {'fill' : None, 'width' : 0} )
if configs:
self._canvas._canvas.itemconfigure(self._object, **configs)
class _RenderedPolygon(_RenderedFillableShape):
def __init__(self, chain, properties):
_RenderedFillableShape.__init__(self, chain, properties)
transform = _graphicsManager._renderedHierarchy.getNode(self._chain)._cumulativeTransformation
self._points = properties['points']
if len(self._points) > 1:
tkPts = [(transform.image(p).getX(),transform.image(p).getY()) for p in self._points]
else:
tkPts = [(0,0)] * 3
self._object = self._canvas._canvas.create_polygon(tkPts)
_graphicsManager._objectIdRegistry[(self._canvas._canvas,self._object)] = self
if 'smooth' in properties:
self._canvas._canvas.itemconfigure(self._object, smooth=1)
_RenderedFillableShape.update(self, properties)
if not self._points: # make effectively invisible TK object with width 0
self._canvas._canvas.itemconfigure(self._object, fill=None, width=0)
def update(self, properties):
_RenderedFillableShape.update(self, properties)
configs = {}
if 'transformation' in properties or 'points' in properties:
wasEmpty = len(self._points) < 2
self._points = properties.get('points', self._points) # update if given
if len(self._points) > 1:
transform = _graphicsManager._renderedHierarchy.getNode(self._chain)._cumulativeTransformation
tkCoords = []
for p in self._points:
tkCoords.append(transform.image(p).getX())
tkCoords.append(transform.image(p).getY())
tkCoords = tuple(tkCoords)
if wasEmpty: # need to explicitly (re)set border properties
configs['width'] = self._transWidth
configs['outline'] = Color._getTkColor(self._borderColor)
configs['fill'] = Color._getTkColor(self._fillColor)
else:
tkCoords = 6 * (0,)
self._canvas._canvas.coords(self._object,tuple(tkCoords))
if not self._points: # make effectively invisible TK object with width 0
configs.update( {'fill' : None, 'width' : 0} )
if configs:
self._canvas._canvas.itemconfigure(self._object, **configs)
class _RenderedText(_RenderedDrawable):
normalFactor = 1.0 # re-configured at startup so that 12pt font has approximate height 16-pixels (96 PPI)
def __init__(self, chain, properties):
_RenderedDrawable.__init__(self, chain, properties)
transform = _graphicsManager._renderedHierarchy.getNode(chain)._cumulativeTransformation
parentTransform = _graphicsManager._renderedHierarchy.getNode(chain[:-1])._cumulativeTransformation
parentScale = parentTransform.scale()
if not transform.scaleAndTranslate() and parentScale > 0:
raise GraphicsError('text cannot be rotated or sheared unless Python Image Library is installed', True)
center = transform.image(Point(0.,0.))
self._renderedSize = properties['font size']
actualSize = int(round(parentScale * self._renderedSize * _RenderedText.normalFactor))
self._object = self._canvas._canvas.create_text(center.get(), text=properties['message'],
anchor='center', justify=properties['justify'],
fill=Color._getTkColor(properties['font color']),
font=('Helvetica', actualSize, 'normal') )
_graphicsManager._objectIdRegistry[(self._canvas._canvas,self._object)] = self
_RenderedDrawable.update(self, properties)
def update(self, properties):
if 'message' in properties:
self._canvas._canvas.itemconfigure(self._object, text=properties['message'])
if 'font color' in properties:
self._canvas._canvas.itemconfigure(self._object, fill=Color._getTkColor(properties['font color']))
if 'justify' in properties:
self._canvas._canvas.itemconfigure(self._object, justify=properties['justify'])
if 'font size' in properties:
self._renderedSize = properties['font size']
# will handle the actual resizeing of tkinter in a moment...
if 'font size' in properties or 'transformation' in properties:
# determine effective size
parentTransform = _graphicsManager._renderedHierarchy.getNode(self._chain[:-1])._cumulativeTransformation
parentScale = parentTransform.scale()
if parentScale < 0:
# raise GraphicsError('text cannot be reflected unless Python Image Library is installed', True)
raise GraphicsError('text cannot be reflected', True)
actualSize = int(round(parentScale * self._renderedSize * _RenderedText.normalFactor))
self._canvas._canvas.itemconfigure(self._object, font=('Helvetica', actualSize, 'normal'))
if 'transformation' in properties:
# consider translation of text center
transform = _graphicsManager._renderedHierarchy.getNode(self._chain)._cumulativeTransformation
if not transform.scaleAndTranslate():
# raise GraphicsError('text cannot be rotated or sheared unless Python Image Library is installed', True)
raise GraphicsError('text cannot be rotated or sheared', True)
center = transform.image(Point(0.,0.))
self._canvas._canvas.coords(self._object, center.getX(), center.getY())
_RenderedDrawable.update(self, properties)
class _RenderedImage(_RenderedDrawable):
# need to make sure that this instance buffers the most recently used
# image, transform, and (data,alpha) arrays.
def __init__(self, chain, properties):
_RenderedDrawable.__init__(self, chain, properties)
self._w = properties['width'] # needed for _buildImage
self._h = properties['height']
self._lastData = self._lastAlpha = None
transform = _graphicsManager._renderedHierarchy.getNode(chain)._cumulativeTransformation
center = transform.image(Point(0.,0.))
if properties['data'] or not transform.translateOnly():
# will need to construct image manually
if properties['data']:
data,alpha = properties['data'], properties['alpha']
else:
data,alpha = _convertImage(properties['image'])
self._lastData, self._lastAlpha = data,alpha
img = self._buildImage(data, alpha, transform)
else:
img = properties['image']
self._lastCumulative = transform # used to recognize translationOnly updates
self._lastImage = img # keep reference to avoid garbage collection
self._object = self._canvas._canvas.create_image(center.get(), image=img, anchor='center')
_graphicsManager._objectIdRegistry[(self._canvas._canvas,self._object)] = self
_RenderedDrawable.update(self, properties)
def update(self, properties):
mustRebuild = 'data' in properties
if mustRebuild or 'transformation' in properties:
transform = _graphicsManager._renderedHierarchy.getNode(self._chain)._cumulativeTransformation
delta = transform * self._lastCumulative.inv()
mustRebuild = mustRebuild or not delta.translateOnly()
self._lastCumulative = transform
if not mustRebuild:
# can get away with simple translation with existing image
center = transform.image(Point(0.,0.))
self._canvas._canvas.coords(self._object, center.getX(), center.getY())
if mustRebuild:
if 'data' in properties:
data,alpha = properties['data'], properties['alpha']
elif self._lastData is not None:
data,alpha = self._lastData, self._lastAlpha
else:
data,alpha = _convertImage(self._lastImage)
self._lastData, self._lastAlpha = data,alpha
self._image = self._buildImage(data, alpha, transform)
center = transform.image(Point(0,0))
self._canvas._canvas.itemconfigure(self._object, image=self._image)
_RenderedDrawable.update(self, properties)
def _buildImage(self, data, alpha, transform):
"""Returns a new PhotoImage instance based on the transformed low-level data arrays."""
# TODO: find ways to batch so that there are less individual calls to img.put
minX = maxX = minY = maxY = None
for x,y in ( (self._w,0), (0,self._h), (self._w,self._h), (0,0)): # do origin last!
p = transform.image(Point(x,y))
if minX is None or minX > p.getX():
minX = p.getX()
if maxX is None or maxX < p.getX():
maxX = p.getX()
if minY is None or minY > p.getY():
minY = p.getY()
if maxY is None or maxY < p.getY():
maxY = p.getY()
offset = Point(p.getX()-minX, p.getY()-minY)
rW = int(round(maxX-minX))
rH = int(round(maxY-minY))
img = _Tkinter.PhotoImage(width=rW, height=rH)
img.blank() # TODO: is this necessary for newly constructed image?
for y in range(rH):
for x in range(rW):
result = transform.inv().image(Point(x+minX,y+minY))
# no anti-aliasing in this version
vx = int(round(result.getX()))
vy = int(round(result.getY()))
if 0 <= vx < self._w and 0 <= vy < self._h:
a,b = divmod(self._w * vy + vx, 8)
if alpha[a] & (1 << b):
color = '#%02x%02x%02x'%tuple(data[3*(vx+self._w*vy):3*(1+vx+self._w*vy)])
img.put(data=color, to=(x,y))
return img
# Library initialization and shutdown
def _initLibrary():
global _tkroot
try:
_tkroot = _Tkinter.Tk()
except KeyboardInterrupt:
raise
except:
raise Exception('unable to start Tkinter on your system')
_graphicsManager._state = 'Failed'
_tkroot.withdraw()
actual = _getTextSize('X', 36)[1]
_RenderedText.normalFactor *= 48.0 / actual # this normalizes so that 36-pt font has 48-pixel height (96 PPI)
def _startCommandThread():
_initLibrary()
while _graphicsManager._state == 'Running':
_graphicsManager.processCommands()
_graphicsManager.processEvents()
_tkroot.update()
_time.sleep(.01)
def _stopCommandThread():
while len(_graphicsManager._openCanvases) > 0:
_time.sleep(.25)
_graphicsManager._state = 'Stopped'
_time.sleep(.25)
def _exitMainThread():
# Main loop will return when all open canvases closed
if _graphicsManager._handlingEvents == 'No':
_graphicsManager._handlingEvents = 'Yes'
if len(_graphicsManager._openCanvases) > 0:
print('Close canvas windows to end program.')
_graphicsManager.mainLoop(None, True)
def startEventHandling():
"""
Blocks the main thread and enters event-handling mode.
This can be counteracted by a later call to stopEventHandling().
Note: This should not be called if using native threading.
"""
if not _nativeThreading:
if _graphicsManager._handlingEvents == 'No':
_graphicsManager._handlingEvents = 'Yes'
_graphicsManager.mainLoop()
def stopEventHandling():
"""
Counteracts an earlier call to startEventHandling().
"""
if not _nativeThreading:
if _graphicsManager._handlingEvents == 'Yes':
_graphicsManager._handlingEvents = 'No'
_graphicsManager = _GraphicsManager()
# Utility for Text. This is used once at startup to normalize measure
# and subsequently to support calls to Text.getDimensions()
# it presumes that the lock is already held for the graphics thread
def _getTextSize(message, fontsize):
tkWin = _Tkinter.Toplevel()
canvas = _Tkinter.Canvas(tkWin)
size = int(round(fontsize * _RenderedText.normalFactor))
i = canvas.create_text(0, 0, text=message, font=('Helvetica', size, 'normal') )
bbox = canvas.bbox(i)
canvas.delete(i)
tkWin.withdraw()
return (bbox[2]-bbox[0],bbox[3]-bbox[1])
# Utility for Image Processing
def _convertImage(img):
"""Takes a PhotoImage instance, and produces array pixmap representation.
Formally, returns pair of arrays.
First is array of bytes describing the colors (in row-major
order), and a second array that is used as a bitfield for
transparency representation.
"""
w = img.width()
h = img.height()
a = _array('B', [0]) * (3*w*h)
t = _array('B', [255]) * ((w*h+7)//8) # all True, for lack of better idea
for x in range(w):
for y in range(h):
color = img.get(x,y)
base = 3 * (y*w + x)
a[base:base+3] = _array('B', [int(v) for v in color.split()])
# appears to be no way to differentiate between black and transparent in this context
# If we knew it was transparent, the following is the proper code.
# if color == '0 0 0':
# u,v = divmod(y*w+x,8)
# t[u] &= (255 - (1 << v)) # make transparent
return (a,t)
Assignment Code
from math import sin, cos, atan2, radians, sqrt
class GeoPosition:
"""Represents a geographic position in terms of latitude and longitude."""
def __init__(self, latitude, longitude):
"""Initialize a GeoPosition having the given latitude and longitude."""
self._lat = latitude
self._long = longitude
def __str__(self):
"""Produce a string representation of the form <latitude, longitude>"""
return '<%8.4f, %8.4f>' % (self._lat, self._long)
def longitude(self):
"""Return the longitude of the position."""
return self._long
def latitude(self):
"""Return the latitude of the position."""
return self._lat
def distance(self, other):
"""Return the great circle distance (in miles) between this position and another.
Uses the "haversine" formula.
http://en.wikipedia.org/wiki/Haversine_formula
"""
earth_radius = 3963.2 # miles
lat1 = radians(self._lat)
lat2 = radians(other._lat)
lon1 = radians(self._long)
lon2 = radians(other._long)
dlat, dlon = lat2-lat1, lon2-lon1
a = sin(dlat/2) ** 2 + sin(dlon/2) ** 2 * cos(lat1) * cos(lat2)
c = 2 * atan2(sqrt(a), sqrt(1-a));
return earth_radius * c;
def project(self):
"""Return an (x,y) tuple representing a planar projection of the U.S. location.
This relies on an Albers pojection.
Derived from Mike Bostock's Albers javascript implementation for D3
http://mbostock.github.com/d3
http://mathworld.wolfram.com/AlbersEqual-AreaConicProjection.html
"""
if self._lat < 25:
return _hawaii(self)
elif self._lat > 51:
return _alaska(self)
else:
return _lower48(self)
def _albers_projection(origin, parallels, translate, scale):
"""Return an Albers projection from geographic positions to x-y positions.
Derived from Mike Bostock's Albers javascript implementation for D3
http://mbostock.github.com/d3
http://mathworld.wolfram.com/AlbersEqual-AreaConicProjection.html
origin -- a geographic position
parallels -- bounding latitudes
translate -- x-y translation to place the projection within a larger map
scale -- scaling factor
"""
phi1, phi2 = [radians(p) for p in parallels]
base_lat = radians(origin.latitude())
s, c = sin(phi1), cos(phi1)
base_lon = radians(origin.longitude())
n = 0.5 * (s + sin(phi2))
C = c*c + 2*n*s
p0 = sqrt(C - 2*n*sin(base_lat))/n
def project(position):
lat, lon = radians(position.latitude()), radians(position.longitude())
t = n * (lon - base_lon)
p = sqrt(C - 2*n*sin(lat))/n
x = scale * p * sin(t) + translate[0]
y = scale * (p * cos(t) - p0) + translate[1]
return (x, y)
return project
_lower48 = _albers_projection(GeoPosition(38, -98), [29.5, 45.5], [480,250], 1000)
_alaska = _albers_projection(GeoPosition(60, -160), [55,65], [150,440], 400)
_hawaii = _albers_projection(GeoPosition(20, -160), [8,18], [300,450], 1000)
Assignment Code
"""Functions for reading data from the sentiment dictionary and tweet files."""
import os
import re
import string
import codecs
from datetime import datetime
from tweet import Tweet
from geo import GeoPosition
DATA_PATH = 'data' + os.sep
def load_sentiments(file_name="data"+os.sep+"sentiments.csv"):
"""Read the sentiment file and return a dictionary containing the sentiment
score of each word, a value from -1 to +1.
"""
sentiments = {}
for line in codecs.open(file_name, encoding='utf8'):
word, score = line.split(',')
sentiments[word] = float(score.strip())
return sentiments
def file_name_for_term(term):
"""Return a valid filename that corresponds to an arbitrary term string."""
valid_characters = '-_' + string.ascii_letters + string.digits
no_space = term.replace(' ', '_')
return ''.join(c for c in no_space if c in valid_characters) + '.txt'
'''
def generate_filtered_file(unfiltered_name, term):
"""Return the path to a file containing tweets that match term, generating
that file if necessary.
"""
filtered_path = DATA_PATH + file_name_for_term(term)
if not os.path.exists(filtered_path):
print('Generating filtered tweets file for %s.' % term)
r = re.compile('W' + term + 'W', flags=re.IGNORECASE)
out = codecs.open(filtered_path, mode='w', encoding='utf8')
unfiltered = codecs.open(DATA_PATH + unfiltered_name, encoding='utf8')
matches = [l for l in unfiltered if term in l.lower()]
for line in matches:
if r.search(line):
out.write(line)
out.close()
return filtered_path
def load_tweets(term='my job', file_name='all_tweets.txt'):
"""Return the list of tweets in file_name that contain term."""
term = term.lower()
filtered_path = generate_filtered_file(file_name, term)
tweets = []
for line in codecs.open(filtered_path, encoding='utf8'):
if len(line.strip().split(" ")) >=4:
loc, _, time_text, text = line.strip().split(" ")
time = datetime.strptime(time_text, '%Y-%m-%d %H:%M:%S')
lat, lon = eval(loc)
tweet = Tweet(text.lower(), time, GeoPosition(lat,lon) )
tweets.append(tweet)
return tweets
'''
Assignment Code
from geo import GeoPosition
class State:
"""Represent a state, including geographic information."""
def __init__(self, code, boundaries):
self._code = code
self._boundaries = []
for b in boundaries:
self._boundaries.append( tuple(GeoPosition(lat,long) for (lat,long) in b) )
self._centroid = self._computeCentroid()
def abbrev(self):
"""Return the two letter abbreviation for the state."""
return self._code
def centroid(self):
"""Return a GeoPosition that represents the centroid of the state."""
return self._centroid
def numBoundaries(self):
"""Return the number of distinct boundaries defining the state."""
return len(self._boundaries)
def getBoundary(self, index):
"""Return a tuple of GeoPositions defining the boundary of given index."""
return self._boundaries[index]
def _computeCentroid(self):
"""Compute and return the centroid of the state upon initialization."""
# First, for each individual boundary we compute its area and centroid
# (Note: we will be loose and treat the geopositions as planar for this computation)
results = []
for b in self._boundaries:
cx = cy = a = 0
for k in range(len(b)):
# consider point k and the one before it (knowing that -1 maps to last point)
temp = (b[k-1].longitude() * b[k].latitude() - b[k].longitude() * b[k-1].latitude())
a += temp
cx += temp * (b[k-1].longitude() + b[k].longitude())
cy += temp * (b[k-1].latitude() + b[k].latitude())
if a != 0:
a *= 0.5
cx /= (6*a)
cy /= (6*a)
a = abs(a)
results.append( (cy,cx,a) )
# For states that have two or more pieces, we compute a composite centroid as a
# weighted average
if len(results) > 1:
cy = sum( results[k][0]*results[k][2] for k in range(len(results)) )
cx = sum( results[k][1]*results[k][2] for k in range(len(results)) )
total = sum( entry[2] for entry in results )
geo = GeoPosition(cy/total, cx/total)
else:
geo = GeoPosition(results[0][0], results[0][1])
return geo
def load_states():
"""Return a dictionary mapping two-letter state abbreviations to State instaces for the US."""
from us_states import us_states
return [State(k,v) for k,v in us_states.items() if k != 'PR'] # ignore Puerto Rico
Assignment Code
import sys
from state import load_states
from country import Country
from parse import load_sentiments
from colors import get_sentiment_color
class SentimentAnalysis:
def __init__(self):
self.sentiments = load_sentiments()
self.states = load_states()
def showCountry(self):
self.usa = Country(self.states, 1200)
#finish
if __name__ == "__main__":
if len(sys.argv) > 1:
query = ' '.join(sys.argv[1:])
print query
else:
print "error"
sa = SentimentAnalysis()
sa.showCountry()
#finish
Assignment Code
class Tweet:
"""Represents a single Tweet, including time and location meta data."""
def __init__(self, message, timestamp, position):
"""
Initialize a Tweet instance.
message - a string that includes the full body of the tweet, including hashtags
timestamp - a datetime.datetime instance describing when the tweet was posted
position - a GeoPosition instance describing the location of the tweet
"""
self._msg = message
self._time = timestamp
self._pos = position
def message(self):
"""Return a string that comprises the full body of the tweet."""
return self._msg
def timestamp(self):
"""Return a datetime.datetime instance describing when the tweet was posted."""
return self._time
def position(self):
"""Return a GeoPosition instance describing the location of the tweet."""
return self._pos
Assignment Code
"""
Defines us_states as a dictionary mapping each state code to its geographic boundary.
State codes (e.g., 'MO'), serve as keys to the dictionary.
The value associated with a state is a list of lists, with each list composed of
(latitude,longitude) tuples defining the boundary of one contiguous portion of the
state. Many states have a single boundary, but some states (e.g., 'HI') have multiple
boundaries.
"""
us_states = {'WA': [[(49.000239, -117.033359), (47.762451, -117.044313), (46.426077, -117.038836), (46.343923, -117.055267), (46.168661, -116.92382), (45.993399, -116.918344), (45.998876, -118.988627), (45.933153, -119.125551), (45.911245, -119.525367), (45.823614, -119.963522), (45.725029, -120.209985), (45.697644, -120.505739), (45.746937, -120.637186), (45.604536, -121.18488), (45.670259, -121.217742), (45.725029, -121.535404), (45.708598, -121.809251), (45.549767, -122.247407), (45.659305, -122.762239), (45.960537, -122.811531), (46.08103, -122.904639), (46.185092, -123.11824), (46.174138, -123.211348), (46.146753, -123.370179), (46.261769, -123.545441), (46.300108, -123.72618), (46.239861, -123.874058), (46.327492, -124.065751), (46.464416, -124.027412), (46.535616, -123.895966), (46.74374, -124.098612), (47.285957, -124.235536), (47.357157, -124.31769), (47.740543, -124.427229), (47.88842, -124.624399), (48.184175, -124.706553), (48.381345, -124.597014), (48.288237, -124.394367), (48.162267, -123.983597), (48.167744, -123.704273), (48.118452, -123.424949), (48.167744, -123.162056), (48.080113, -123.036086), (48.08559, -122.800578), (47.866512, -122.636269), (47.882943, -122.515777), (47.587189, -122.493869), (47.318818, -122.422669), (47.346203, -122.324084), (47.576235, -122.422669), (47.800789, -122.395284), (48.030821, -122.230976), (48.123929, -122.362422), (48.288237, -122.373376), (48.468976, -122.471961), (48.600422, -122.422669), (48.753777, -122.488392), (48.775685, -122.647223), (48.8907, -122.795101), (49.000239, -122.756762), (49.000239, -117.033359)], [(48.310145, -122.718423), (48.35396, -122.586977), (48.151313, -122.608885), (48.227991, -122.767716), (48.310145, -122.718423)], [(48.583992, -123.025132), (48.715438, -122.915593), (48.556607, -122.767716), (48.419683, -122.811531), (48.458022, -123.041563), (48.583992, -123.025132)]], 'DE': [[(39.804456, -75.414089), (39.683964, -75.507197), (39.61824, -75.611259), (39.459409, -75.589352), (39.311532, -75.441474), (39.065069, -75.403136), (38.807653, -75.189535), (38.796699, -75.09095), (38.451652, -75.047134), (38.462606, -75.693413), (39.722302, -75.786521), (39.831841, -75.616736), (39.804456, -75.414089)]], 'DC': [[(38.993869, -77.035264), (38.895284, -76.909294), (38.791222, -77.040741), (38.933623, -77.117418), (38.993869, -77.035264)]], 'WI': [[(46.568478, -90.415429), (46.508231, -90.229213), (46.338446, -90.119674), (46.135799, -89.09001), (45.987922, -88.662808), (46.020784, -88.531362), (45.922199, -88.10416), (45.796229, -87.989145), (45.675736, -87.781021), (45.500474, -87.791975), (45.363551, -87.885083), (45.341643, -87.649574), (45.199243, -87.742682), (45.095181, -87.589328), (44.974688, -87.627666), (44.95278, -87.819359), (44.722749, -87.983668), (44.563917, -88.043914), (44.536533, -87.928898), (44.640595, -87.775544), (44.837764, -87.611236), (44.914442, -87.403112), (45.166381, -87.238804), (45.22115, -87.03068), (45.089704, -87.047111), (44.969211, -87.189511), (44.552964, -87.468835), (44.322932, -87.545512), (44.158624, -87.540035), (44.103854, -87.644097), (43.8793, -87.737205), (43.687607, -87.704344), (43.561637, -87.791975), (43.249452, -87.912467), (43.002989, -87.885083), (42.783912, -87.76459), (42.493634, -87.802929), (42.493634, -88.788778), (42.510065, -90.639984), (42.636034, -90.711184), (42.75105, -91.067185), (42.909881, -91.143862), (43.134436, -91.176724), (43.254929, -91.056231), (43.353514, -91.204109), (43.501391, -91.215062), (43.616407, -91.269832), (43.775238, -91.242447), (43.994316, -91.43414), (44.032654, -91.592971), (44.202439, -91.877772), (44.333886, -91.927065), (44.443425, -92.233773), (44.552964, -92.337835), (44.569394, -92.545959), (44.750133, -92.808852), (45.117088, -92.737652), (45.286874, -92.75956), (45.440228, -92.644544), (45.566198, -92.770513), (45.577151, -92.885529), (45.719552, -92.869098), (45.933153, -92.639067), (46.015307, -92.354266), (46.075553, -92.29402), (46.667063, -92.29402), (46.749217, -92.091373), (46.705401, -92.014696), (46.694447, -91.790141), (46.864232, -91.09457), (46.95734, -90.837154), (46.88614, -90.749522), (46.754694, -90.886446), (46.584908, -90.55783), (46.568478, -90.415429)]], 'WV': [[(40.636951, -80.518598), (39.722302, -80.518598), (39.722302, -79.477979), (39.20747, -79.488933), (39.300578, -79.291763), (39.470363, -79.094593), (39.437501, -78.963147), (39.585379, -78.765977), (39.514178, -78.470222), (39.623717, -78.431884), (39.61824, -78.267575), (39.694917, -78.174467), (39.601809, -78.004682), (39.601809, -77.834897), (39.322485, -77.719881), (39.130793, -77.82942), (39.464886, -78.349729), (39.169131, -78.404499), (38.763838, -78.870039), (38.851469, -78.996008), (38.495467, -79.209609), (38.413313, -79.313671), (38.457129, -79.477979), (38.594052, -79.647764), (38.364021, -79.724442), (38.177805, -79.921611), (37.997066, -79.998289), (37.849189, -80.184505), (37.690357, -80.294043), (37.509618, -80.29952), (37.421987, -80.474782), (37.482234, -80.513121), (37.290541, -80.967707), (37.235771, -81.225123), (37.339833, -81.362047), (37.208387, -81.55374), (37.20291, -81.679709), (37.285064, -81.849494), (37.454849, -81.986418), (37.537003, -81.969987), (37.553434, -82.101434), (37.668449, -82.293127), (37.783465, -82.342419), (37.931343, -82.50125), (38.123036, -82.621743), (38.424267, -82.594358), (38.446175, -82.331465), (38.577622, -82.293127), (38.632391, -82.172634), (38.785745, -82.221926), (39.026731, -82.03571), (38.873376, -81.887833), (38.966484, -81.783771), (39.0815, -81.811156), (39.273193, -81.685186), (39.267716, -81.57017), (39.410117, -81.455155), (39.344393, -81.345616), (39.388209, -81.219646), (39.711348, -80.830783), (40.078303, -80.737675), (40.319289, -80.600752), (40.472643, -80.595275), (40.582182, -80.666475), (40.636951, -80.518598)]], 'HI': [[(18.948267, -155.634835), (19.035898, -155.881297), (19.123529, -155.919636), (19.348084, -155.886774), (19.73147, -156.062036), (19.857439, -155.925113), (20.032702, -155.826528), (20.147717, -155.897728), (20.26821, -155.87582), (20.12581, -155.596496), (20.021748, -155.284311), (19.868393, -155.092618), (19.736947, -155.092618), (19.523346, -154.807817), (19.348084, -154.983079), (19.26593, -155.295265), (19.134483, -155.514342), (18.948267, -155.634835)], [(21.029505, -156.587823), (20.892581, -156.472807), (20.952827, -156.324929), (20.793996, -156.00179), (20.651596, -156.051082), (20.580396, -156.379699), (20.60778, -156.445422), (20.783042, -156.461853), (20.821381, -156.631638), (20.919966, -156.697361), (21.029505, -156.587823)], [(21.210244, -156.982162), (21.106182, -157.080747), (21.106182, -157.310779), (21.221198, -157.239579), (21.210244, -156.982162)], [(21.697691, -157.951581), (21.462183, -157.842042), (21.325259, -157.896811), (21.303352, -158.110412), (21.582676, -158.252813), (21.588153, -158.126843), (21.697691, -157.951581)], [(22.228955, -159.468693), (22.218001, -159.353678), (22.113939, -159.298908), (21.966061, -159.33177), (21.872953, -159.446786), (21.987969, -159.764448), (22.152277, -159.726109), (22.228955, -159.468693)]], 'FL': [[(30.997536, -85.497137), (31.003013, -85.004212), (30.712735, -84.867289), (30.647012, -83.498053), (30.570335, -82.216449), (30.356734, -82.167157), (30.362211, -82.046664), (30.564858, -82.002849), (30.751074, -82.041187), (30.827751, -81.948079), (30.745597, -81.718048), (30.707258, -81.444201), (30.27458, -81.383954), (29.787132, -81.257985), (29.14633, -80.967707), (28.461713, -80.524075), (28.41242, -80.589798), (28.094758, -80.56789), (27.738757, -80.381674), (27.021277, -80.091397), (26.796723, -80.03115), (26.566691, -80.036627), (25.739673, -80.146166), (25.723243, -80.239274), (25.465826, -80.337859), (25.383672, -80.304997), (25.197456, -80.49669), (25.241272, -80.573367), (25.164595, -80.759583), (25.120779, -81.077246), (25.224841, -81.170354), (25.378195, -81.126538), (25.821827, -81.351093), (25.903982, -81.526355), (25.843735, -81.679709), (26.090198, -81.800202), (26.292844, -81.833064), (26.517399, -82.041187), (26.665276, -82.09048), (26.878877, -82.057618), (26.917216, -82.172634), (26.791246, -82.145249), (26.758384, -82.249311), (27.300601, -82.566974), (27.437525, -82.692943), (27.837342, -82.391711), (27.815434, -82.588881), (27.689464, -82.720328), (27.886634, -82.851774), (28.434328, -82.676512), (28.888914, -82.643651), (28.998453, -82.764143), (29.14633, -82.802482), (29.179192, -82.994175), (29.420177, -83.218729), (29.518762, -83.399469), (29.66664, -83.410422), (29.721409, -83.536392), (29.885717, -83.640454), (30.104795, -84.02384), (30.055502, -84.357933), (29.902148, -84.341502), (29.929533, -84.451041), (29.743317, -84.867289), (29.699501, -85.310921), (29.80904, -85.299967), (29.940487, -85.404029), (30.236241, -85.924338), (30.362211, -86.29677), (30.395073, -86.630863), (30.373165, -86.910187), (30.280057, -87.518128), (30.427934, -87.37025), (30.510088, -87.446927), (30.674397, -87.408589), (30.86609, -87.633143), (30.997536, -87.600282), (30.997536, -85.497137)]], 'WY': [[(45.002073, -109.080842), (45.002073, -105.91517), (44.996596, -104.058488), (43.002989, -104.053011), (41.003906, -104.053011), (40.998429, -105.728954), (41.003906, -107.919731), (40.998429, -109.04798), (40.998429, -111.047063), (42.000709, -111.047063), (44.476286, -111.047063), (45.002073, -111.05254), (45.002073, -109.080842)]], 'NH': [[(45.303304, -71.08183), (44.657025, -71.032537), (43.34256, -70.966814), (43.227544, -70.807983), (43.128959, -70.824413), (43.057759, -70.703921), (42.871543, -70.818936), (42.887974, -70.917521), (42.789389, -71.185891), (42.696281, -71.29543), (42.729142, -72.456542), (42.80582, -72.544173), (42.953697, -72.533219), (43.008466, -72.445588), (43.150867, -72.456542), (43.572591, -72.379864), (43.769761, -72.204602), (43.994316, -72.116971), (44.07647, -72.02934), (44.322932, -72.034817), (44.41604, -71.700724), (44.585825, -71.536416), (44.750133, -71.629524), (44.914442, -71.4926), (45.013027, -71.503554), (45.270443, -71.361154), (45.243058, -71.131122), (45.303304, -71.08183)]], 'NJ': [[(41.14083, -74.236547), (40.998429, -73.902454), (40.708151, -74.022947), (40.642428, -74.187255), (40.489074, -74.274886), (40.412397, -74.001039), (40.297381, -73.979131), (39.760641, -74.099624), (39.360824, -74.411809), (39.245808, -74.614456), (38.993869, -74.795195), (39.158177, -74.888303), (39.240331, -75.178581), (39.459409, -75.534582), (39.607286, -75.55649), (39.629194, -75.561967), (39.683964, -75.507197), (39.804456, -75.414089), (39.88661, -75.145719), (39.963288, -75.129289), (40.127596, -74.82258), (40.215227, -74.773287), (40.417874, -75.058088), (40.543843, -75.069042), (40.576705, -75.195012), (40.691721, -75.205966), (40.866983, -75.052611), (40.971045, -75.134765), (41.179168, -74.882826), (41.288707, -74.828057), (41.359907, -74.69661), (41.14083, -74.236547)]], 'NM': [[(37.000263, -107.421329), (36.994786, -106.868158), (36.994786, -104.337812), (37.000263, -103.001438), (36.501861, -103.001438), (36.501861, -103.039777), (34.01533, -103.045254), (33.002096, -103.067161), (31.999816, -103.067161), (31.999816, -106.616219), (31.901231, -106.643603), (31.786216, -106.528588), (31.786216, -108.210008), (31.331629, -108.210008), (31.331629, -109.04798), (37.000263, -109.042503), (37.000263, -107.421329)]], 'TX': [[(36.501861, -101.812942), (36.501861, -100.000075), (34.563024, -100.000075), (34.573978, -99.923398), (34.382285, -99.698843), (34.415147, -99.57835), (34.404193, -99.260688), (34.2125, -99.189488), (34.223454, -98.986841), (34.135823, -98.767763), (34.146777, -98.570593), (34.064623, -98.488439), (34.157731, -98.36247), (34.113915, -98.170777), (34.004376, -98.088623), (33.987946, -97.946222), (33.851022, -97.869545), (33.982469, -97.694283), (33.905791, -97.458774), (33.823637, -97.371143), (33.861976, -97.256128), (33.736006, -97.173974), (33.960561, -96.922034), (33.845545, -96.850834), (33.845545, -96.631756), (33.774345, -96.423633), (33.686714, -96.346956), (33.840068, -96.149786), (33.889361, -95.936185), (33.834591, -95.8376), (33.933176, -95.602092), (33.878407, -95.547322), (33.87293, -95.289906), (33.960561, -95.224183), (33.861976, -94.966767), (33.74696, -94.868182), (33.637421, -94.484796), (33.544313, -94.380734), (33.593606, -94.183564), (33.54979, -94.041164), (33.018527, -94.041164), (31.994339, -94.041164), (31.775262, -93.822086), (31.556184, -93.816609), (31.15089, -93.542762), (30.93729, -93.526331), (30.679874, -93.630393), (30.575812, -93.728978), (30.438888, -93.696116), (30.334826, -93.767317), (30.143133, -93.690639), (29.787132, -93.926148), (29.688547, -93.838517), (29.68307, -94.002825), (29.546147, -94.523134), (29.622824, -94.70935), (29.787132, -94.742212), (29.672117, -94.873659), (29.699501, -94.966767), (29.557101, -95.016059), (29.496854, -94.911997), (29.310638, -94.895566), (29.113469, -95.081782), (28.867006, -95.383014), (28.604113, -95.985477), (28.647929, -96.045724), (28.582205, -96.226463), (28.642452, -96.23194), (28.598636, -96.478402), (28.724606, -96.593418), (28.697221, -96.664618), (28.439805, -96.401725), (28.357651, -96.593418), (28.406943, -96.774157), (28.226204, -96.801542), (28.039988, -97.026096), (27.694941, -97.256128), (27.333463, -97.404005), (27.360848, -97.513544), (27.229401, -97.540929), (27.262263, -97.425913), (26.99937, -97.480682), (26.988416, -97.557359), (26.840538, -97.562836), (26.758384, -97.469728), (26.457153, -97.442344), (26.353091, -97.332805), (26.161398, -97.30542), (25.991613, -97.217789), (25.887551, -97.524498), (26.018997, -97.650467), (26.06829, -97.885976), (26.057336, -98.198161), (26.221644, -98.466531), (26.238075, -98.669178), (26.369522, -98.822533), (26.413337, -99.030656), (26.539307, -99.173057), (26.840538, -99.266165), (27.021277, -99.446904), (27.174632, -99.424996), (27.33894, -99.50715), (27.48134, -99.479765), (27.640172, -99.605735), (27.656603, -99.709797), (27.799003, -99.879582), (27.979742, -99.934351), (28.14405, -100.082229), (28.280974, -100.29583), (28.582205, -100.399891), (28.66436, -100.498476), (28.905345, -100.629923), (29.102515, -100.673738), (29.244915, -100.799708), (29.370885, -101.013309), (29.458516, -101.062601), (29.535193, -101.259771), (29.754271, -101.413125), (29.803563, -101.851281), (29.792609, -102.114174), (29.869286, -102.338728), (29.765225, -102.388021), (29.732363, -102.629006), (29.524239, -102.809745), (29.190146, -102.919284), (29.184669, -102.97953), (28.987499, -103.116454), (28.982022, -103.280762), (29.135376, -103.527224), (29.381839, -104.146119), (29.513285, -104.266611), (29.639255, -104.507597), (29.924056, -104.677382), (30.181472, -104.688336), (30.389596, -104.858121), (30.570335, -104.896459), (30.685351, -105.005998), (30.855136, -105.394861), (31.085167, -105.602985), (31.167321, -105.77277), (31.364491, -105.953509), (31.468553, -106.205448), (31.731446, -106.38071), (31.786216, -106.528588), (31.901231, -106.643603), (31.999816, -106.616219), (31.999816, -103.067161), (33.002096, -103.067161), (34.01533, -103.045254), (36.501861, -103.039777), (36.501861, -103.001438), (36.501861, -101.812942)]], 'LA': [[(33.018527, -93.608485), (33.002096, -91.16577), (32.887081, -91.072662), (32.843265, -91.143862), (32.640618, -91.154816), (32.514649, -91.006939), (32.218894, -90.985031), (31.988862, -91.105524), (31.846462, -91.341032), (31.621907, -91.401278), (31.643815, -91.499863), (31.27686, -91.516294), (31.265906, -91.636787), (31.068736, -91.565587), (30.997536, -91.636787), (30.997536, -89.747242), (30.66892, -89.845827), (30.449842, -89.681519), (30.285534, -89.643181), (30.181472, -89.522688), (30.044549, -89.818443), (29.945964, -89.84035), (29.88024, -89.599365), (30.039072, -89.495303), (29.88024, -89.287179), (29.754271, -89.30361), (29.699501, -89.424103), (29.748794, -89.648657), (29.655686, -89.621273), (29.513285, -89.69795), (29.387316, -89.506257), (29.348977, -89.199548), (29.2011, -89.09001), (29.179192, -89.002379), (29.009407, -89.16121), (29.042268, -89.336472), (29.217531, -89.484349), (29.310638, -89.851304), (29.480424, -89.851304), (29.425654, -90.032043), (29.283254, -90.021089), (29.151807, -90.103244), (29.129899, -90.23469), (29.277777, -90.333275), (29.283254, -90.563307), (29.129899, -90.645461), (29.086084, -90.798815), (29.179192, -90.963123), (29.190146, -91.09457), (29.436608, -91.220539), (29.546147, -91.445094), (29.529716, -91.532725), (29.73784, -91.620356), (29.710455, -91.883249), (29.836425, -91.888726), (29.715932, -92.146142), (29.622824, -92.113281), (29.535193, -92.31045), (29.579009, -92.617159), (29.715932, -92.97316), (29.776178, -93.2251), (29.726886, -93.767317), (29.688547, -93.838517), (29.787132, -93.926148), (30.143133, -93.690639), (30.334826, -93.767317), (30.438888, -93.696116), (30.575812, -93.728978), (30.679874, -93.630393), (30.93729, -93.526331), (31.15089, -93.542762), (31.556184, -93.816609), (31.775262, -93.822086), (31.994339, -94.041164), (33.018527, -94.041164), (33.018527, -93.608485)]], 'NC': [[(36.562108, -80.978661), (36.545677, -80.294043), (36.5402, -79.510841), (36.551154, -75.868676), (36.151337, -75.75366), (36.189676, -76.032984), (36.140383, -76.071322), (36.080137, -76.410893), (36.025367, -76.460185), (36.008937, -76.68474), (35.937736, -76.673786), (35.987029, -76.399939), (35.943213, -76.3616), (35.992506, -76.060368), (35.899398, -75.961783), (35.937736, -75.781044), (35.696751, -75.715321), (35.581735, -75.775568), (35.570781, -75.89606), (35.324319, -76.147999), (35.313365, -76.482093), (35.14358, -76.536862), (34.973795, -76.394462), (34.940933, -76.279446), (34.661609, -76.493047), (34.694471, -76.673786), (34.667086, -76.991448), (34.60684, -77.210526), (34.415147, -77.555573), (34.163208, -77.82942), (33.845545, -77.971821), (33.916745, -78.179944), (33.851022, -78.541422), (34.80401, -79.675149), (34.820441, -80.797922), (34.935456, -80.781491), (35.105241, -80.934845), (35.044995, -81.038907), (35.149057, -81.044384), (35.198349, -82.276696), (35.160011, -82.550543), (35.066903, -82.764143), (35.00118, -83.109191), (34.984749, -83.618546), (34.990226, -84.319594), (35.225734, -84.29221), (35.247642, -84.09504), (35.41195, -84.018363), (35.559827, -83.7719), (35.565304, -83.498053), (35.718659, -83.251591), (35.773428, -82.994175), (35.997983, -82.775097), (36.063706, -82.638174), (35.965121, -82.610789), (36.156814, -82.216449), (36.118475, -82.03571), (36.304691, -81.909741), (36.353984, -81.723525), (36.589492, -81.679709), (36.562108, -80.978661)]], 'ND': [[(49.000239, -97.228743), (48.682577, -97.097296), (48.545653, -97.16302), (48.140359, -97.130158), (47.948667, -97.053481), (47.609096, -96.856311), (46.968294, -96.823449), (46.924479, -96.785111), (46.656109, -96.801542), (46.437031, -96.719387), (46.332969, -96.598895), (45.933153, -96.560556), (45.944106, -104.047534), (47.861036, -104.042057), (49.000239, -104.047534), (49.000239, -97.228743)]], 'NE': [[(43.002989, -103.324578), (42.997512, -101.626726), (42.997512, -98.499393), (42.94822, -98.466531), (42.767481, -97.951699), (42.866066, -97.831206), (42.844158, -97.688806), (42.844158, -97.217789), (42.657942, -96.692003), (42.515542, -96.626279), (42.488157, -96.44554), (42.039048, -96.264801), (41.973325, -96.127878), (41.798063, -96.062155), (41.67757, -96.122401), (41.540646, -96.095016), (41.453015, -95.919754), (41.201076, -95.925231), (40.976521, -95.826646), (40.719105, -95.881416), (40.587659, -95.7664), (40.264519, -95.552799), (40.001626, -95.306337), (40.001626, -101.90605), (40.001626, -102.053927), (41.003906, -102.053927), (41.003906, -104.053011), (43.002989, -104.053011), (43.002989, -103.324578)]], 'TN': [[(36.496384, -88.054868), (36.677123, -88.071299), (36.633308, -87.852221), (36.655216, -86.592525), (36.616877, -85.486183), (36.627831, -85.289013), (36.594969, -84.544149), (36.584015, -83.689746), (36.600446, -83.673316), (36.589492, -81.679709), (36.353984, -81.723525), (36.304691, -81.909741), (36.118475, -82.03571), (36.156814, -82.216449), (35.965121, -82.610789), (36.063706, -82.638174), (35.997983, -82.775097), (35.773428, -82.994175), (35.718659, -83.251591), (35.565304, -83.498053), (35.559827, -83.7719), (35.41195, -84.018363), (35.247642, -84.09504), (35.225734, -84.29221), (34.990226, -84.319594), (34.984749, -85.606675), (35.00118, -87.359296), (34.995703, -88.202745), (34.995703, -88.471115), (34.995703, -90.311367), (35.023087, -90.212782), (35.198349, -90.114197), (35.439335, -90.130628), (35.603643, -89.944412), (35.756997, -89.911551), (35.811767, -89.763673), (35.997983, -89.730812), (36.249922, -89.533642), (36.496384, -89.539119), (36.496384, -89.484349), (36.496384, -89.418626), (36.507338, -89.298133), (36.496384, -88.054868)]], 'NY': [[(45.013027, -73.343806), (44.804903, -73.332852), (44.618687, -73.387622), (44.437948, -73.294514), (44.246255, -73.321898), (44.043608, -73.436914), (43.769761, -73.349283), (43.687607, -73.404052), (43.523299, -73.245221), (42.833204, -73.278083), (42.745573, -73.267129), (42.08834, -73.508114), (42.050002, -73.486206), (41.294184, -73.55193), (41.21203, -73.48073), (41.102491, -73.727192), (40.987475, -73.655992), (40.905321, -73.22879), (40.965568, -73.141159), (40.965568, -72.774204), (40.998429, -72.587988), (41.157261, -72.28128), (41.042245, -72.259372), (40.992952, -72.100541), (40.845075, -72.467496), (40.625997, -73.239744), (40.582182, -73.562884), (40.593136, -73.776484), (40.543843, -73.935316), (40.708151, -74.022947), (40.998429, -73.902454), (41.14083, -74.236547), (41.359907, -74.69661), (41.431108, -74.740426), (41.436584, -74.89378), (41.60637, -75.074519), (41.754247, -75.052611), (41.869263, -75.173104), (41.863786, -75.249781), (42.000709, -75.35932), (42.000709, -79.76278), (42.252649, -79.76278), (42.269079, -79.76278), (42.55388, -79.149363), (42.690804, -79.050778), (42.783912, -78.853608), (42.953697, -78.930285), (42.986559, -79.012439), (43.260406, -79.072686), (43.375421, -78.486653), (43.369944, -77.966344), (43.34256, -77.75822), (43.233021, -77.533665), (43.276836, -77.391265), (43.271359, -76.958587), (43.34256, -76.695693), (43.523299, -76.41637), (43.528776, -76.235631), (43.802623, -76.230154), (43.961454, -76.137046), (44.070993, -76.3616), (44.196962, -76.312308), (44.366748, -75.912491), (44.514625, -75.764614), (44.848718, -75.282643), (45.018503, -74.828057), (44.991119, -74.148916), (45.013027, -73.343806)]], 'PA': [[(42.252649, -79.76278), (42.000709, -79.76278), (42.000709, -75.35932), (41.863786, -75.249781), (41.869263, -75.173104), (41.754247, -75.052611), (41.60637, -75.074519), (41.436584, -74.89378), (41.431108, -74.740426), (41.359907, -74.69661), (41.288707, -74.828057), (41.179168, -74.882826), (40.971045, -75.134765), (40.866983, -75.052611), (40.691721, -75.205966), (40.576705, -75.195012), (40.543843, -75.069042), (40.417874, -75.058088), (40.215227, -74.773287), (40.127596, -74.82258), (39.963288, -75.129289), (39.88661, -75.145719), (39.804456, -75.414089), (39.831841, -75.616736), (39.722302, -75.786521), (39.722302, -79.477979), (39.722302, -80.518598), (40.636951, -80.518598), (41.978802, -80.518598), (41.978802, -80.518598), (42.033571, -80.332382), (42.269079, -79.76278), (42.252649, -79.76278)]], 'AK': [[(55.117982, -131.602021), (55.28229, -131.569159), (55.183705, -131.355558), (55.01392, -131.38842), (55.035827, -131.645836), (55.117982, -131.602021)], [(55.42469, -131.832052), (55.304197, -131.645836), (55.128935, -131.749898), (55.189182, -131.832052), (55.42469, -131.832052)], [(56.437924, -132.976733), (56.459832, -132.735747), (56.421493, -132.631685), (56.273616, -132.664547), (56.240754, -132.878148), (56.333862, -133.069841), (56.437924, -132.976733)], [(56.350293, -133.595627), (56.317431, -133.162949), (56.125739, -133.05341), (55.912138, -132.620732), (55.780691, -132.472854), (55.671152, -132.4619), (55.649245, -132.357838), (55.506844, -132.341408), (55.364444, -132.166146), (55.238474, -132.144238), (55.276813, -132.029222), (55.178228, -131.97993), (54.789365, -131.958022), (54.701734, -132.029222), (54.718165, -132.308546), (54.915335, -132.385223), (54.898904, -132.483808), (55.046781, -132.686455), (54.997489, -132.746701), (55.046781, -132.916486), (54.898904, -132.889102), (54.937242, -132.73027), (54.882473, -132.626209), (54.679826, -132.675501), (54.701734, -132.867194), (54.95915, -133.157472), (55.090597, -133.239626), (55.22752, -133.223195), (55.216566, -133.453227), (55.320628, -133.453227), (55.331582, -133.277964), (55.42469, -133.102702), (55.588998, -133.17938), (55.62186, -133.387503), (55.884753, -133.420365), (56.0162, -133.497042), (55.923092, -133.639442), (56.070969, -133.694212), (56.142169, -133.546335), (56.311955, -133.666827), (56.350293, -133.595627)], [(55.556137, -133.738027), (55.490413, -133.546335), (55.572568, -133.414888), (55.534229, -133.283441), (55.386352, -133.420365), (55.430167, -133.633966), (55.556137, -133.738027)], [(56.930849, -133.907813), (57.029434, -134.050213), (57.095157, -133.885905), (57.002049, -133.343688), (57.007526, -133.102702), (56.82131, -132.932917), (56.667956, -132.620732), (56.55294, -132.653593), (56.492694, -132.817901), (56.520078, -133.042456), (56.448878, -133.201287), (56.492694, -133.420365), (56.448878, -133.66135), (56.684386, -133.710643), (56.837741, -133.688735), (56.843218, -133.869474), (56.930849, -133.907813)], [(56.48174, -134.115936), (56.558417, -134.25286), (56.722725, -134.400737), (56.848695, -134.417168), (56.908941, -134.296675), (56.848695, -134.170706), (56.952757, -134.143321), (56.772017, -133.748981), (56.596755, -133.710643), (56.574848, -133.847566), (56.377678, -133.935197), (56.322908, -133.836612), (56.092877, -133.957105), (56.142169, -134.110459), (55.999769, -134.132367), (56.070969, -134.230952), (56.350293, -134.291198), (56.48174, -134.115936)], [(56.28457, -134.636246), (56.169554, -134.669107), (56.235277, -134.806031), (56.67891, -135.178463), (56.810356, -135.413971), (56.914418, -135.331817), (57.166357, -135.424925), (57.369004, -135.687818), (57.566174, -135.419448), (57.48402, -135.298955), (57.418296, -135.063447), (57.407343, -134.849846), (57.248511, -134.844369), (56.728202, -134.636246), (56.28457, -134.636246)], [(58.223407, -134.712923), (58.14673, -134.373353), (58.157683, -134.176183), (58.081006, -134.187137), (57.807159, -133.902336), (57.850975, -134.099505), (57.757867, -134.148798), (57.615466, -133.935197), (57.363527, -133.869474), (57.297804, -134.083075), (57.210173, -134.154275), (57.029434, -134.499322), (57.034911, -134.603384), (57.226604, -134.6472), (57.341619, -134.575999), (57.511404, -134.608861), (57.719528, -134.729354), (57.829067, -134.707446), (58.097437, -134.784123), (58.212453, -134.91557), (58.409623, -134.953908), (58.223407, -134.712923)], [(57.330665, -135.857603), (57.330665, -135.715203), (57.149926, -135.567326), (57.023957, -135.633049), (56.996572, -135.857603), (57.193742, -135.824742), (57.330665, -135.857603)], [(58.206976, -136.279328), (58.201499, -135.978096), (58.28913, -135.780926), (58.168637, -135.496125), (58.037191, -135.64948), (57.987898, -135.59471), (58.135776, -135.45231), (58.086483, -135.107263), (57.976944, -134.91557), (57.779775, -135.025108), (57.763344, -134.937477), (57.500451, -134.822462), (57.462112, -135.085355), (57.675713, -135.572802), (57.456635, -135.556372), (57.369004, -135.709726), (57.407343, -135.890465), (57.544266, -136.000004), (57.637374, -136.208128), (57.829067, -136.366959), (57.916698, -136.569606), (58.075529, -136.558652), (58.130299, -136.421728), (58.267222, -136.377913), (58.206976, -136.279328)], [(60.200582, -147.079854), (59.948643, -147.501579), (59.850058, -147.53444), (59.784335, -147.874011), (59.937689, -147.80281), (60.09652, -147.435855), (60.271782, -147.205824), (60.200582, -147.079854)], [(60.578491, -147.561825), (60.370367, -147.616594), (60.156767, -147.758995), (60.227967, -147.956165), (60.474429, -147.791856), (60.578491, -147.561825)], [(70.245291, -147.786379), (70.201475, -147.682318), (70.15766, -147.162008), (70.185044, -146.888161), (70.185044, -146.510252), (70.146706, -146.099482), (70.168614, -145.858496), (70.08646, -145.622988), (69.993352, -145.195787), (69.971444, -144.620708), (70.026213, -144.461877), (70.059075, -144.078491), (70.130275, -143.914183), (70.141229, -143.497935), (70.091936, -143.503412), (70.119321, -143.25695), (70.042644, -142.747594), (69.916674, -142.402547), (69.856428, -142.079408), (69.801659, -142.008207), (69.790705, -141.712453), (69.697597, -141.433129), (69.63735, -141.378359), (69.686643, -141.208574), (69.648304, -141.00045), (60.304644, -141.00045), (60.22249, -140.53491), (60.310121, -140.474664), (60.184151, -139.987216), (60.342983, -139.696939), (60.359413, -139.088998), (60.091043, -139.198537), (59.997935, -139.045183), (59.910304, -138.700135), (59.767904, -138.623458), (59.242118, -137.604747), (58.908024, -137.445916), (59.001132, -137.265177), (59.159963, -136.827022), (59.16544, -136.580559), (59.285933, -136.465544), (59.466672, -136.476498), (59.466672, -136.301236), (59.625503, -136.25742), (59.663842, -135.945234), (59.800766, -135.479694), (59.565257, -135.025108), (59.422857, -135.068924), (59.280456, -134.959385), (59.247595, -134.701969), (59.033994, -134.378829), (58.973748, -134.400737), (58.858732, -134.25286), (58.727285, -133.842089), (58.152206, -133.173903), (57.998852, -133.075318), (57.845498, -132.867194), (57.505928, -132.560485), (57.21565, -132.253777), (57.095157, -132.368792), (57.051341, -132.05113), (56.876079, -132.127807), (56.804879, -131.870391), (56.602232, -131.837529), (56.613186, -131.580113), (56.405062, -131.087188), (56.366724, -130.78048), (56.268139, -130.621648), (56.240754, -130.468294), (56.142169, -130.424478), (56.114785, -130.101339), (55.994292, -130.002754), (55.769737, -130.150631), (55.583521, -130.128724), (55.276813, -129.986323), (55.200136, -130.095862), (54.920812, -130.336847), (54.718165, -130.687372), (54.822227, -130.785957), (54.789365, -130.917403), (54.997489, -131.010511), (55.08512, -130.983126), (55.189182, -131.092665), (55.298721, -130.862634), (55.337059, -130.928357), (55.200136, -131.158389), (55.287767, -131.284358), (55.238474, -131.426759), (55.457552, -131.843006), (55.698537, -131.700606), (55.616383, -131.963499), (55.49589, -131.974453), (55.588998, -132.182576), (55.704014, -132.226392), (55.829984, -132.083991), (55.955953, -132.127807), (55.851892, -132.324977), (56.076446, -132.522147), (56.032631, -132.642639), (56.218847, -132.719317), (56.339339, -132.527624), (56.339339, -132.341408), (56.487217, -132.396177), (56.67891, -132.297592), (56.673433, -132.450946), (56.837741, -132.768609), (57.034911, -132.993164), (57.177311, -133.51895), (57.577128, -133.507996), (57.62642, -133.677781), (57.790728, -133.639442), (57.834544, -133.814705), (58.053622, -134.072121), (58.168637, -134.143321), (58.206976, -134.586953), (58.502731, -135.074401), (59.192825, -135.282525), (59.033994, -135.38111), (58.891593, -135.337294), (58.617746, -135.140124), (58.573931, -135.189417), (58.349376, -135.05797), (58.201499, -135.085355), (58.234361, -135.277048), (58.398669, -135.430402), (58.426053, -135.633049), (58.382238, -135.91785), (58.617746, -135.912373), (58.814916, -136.087635), (58.75467, -136.246466), (58.962794, -136.876314), (58.902547, -136.931084), (58.836824, -136.586036), (58.672516, -136.317666), (58.667039, -136.213604), (58.535592, -136.180743), (58.382238, -136.043819), (58.294607, -136.388867), (58.349376, -136.591513), (58.212453, -136.59699), (58.316515, -136.859883), (58.393192, -136.947514), (58.393192, -137.111823), (58.590362, -137.566409), (58.765624, -137.900502), (58.869686, -137.933364), (59.02304, -138.11958), (59.132579, -138.634412), (59.247595, -138.919213), (59.379041, -139.417615), (59.505011, -139.746231), (59.641934, -139.718846), (59.598119, -139.625738), (59.68575, -139.5162), (59.88292, -139.625738), (59.992458, -139.488815), (60.041751, -139.554538), (59.833627, -139.801), (59.696704, -140.315833), (59.745996, -140.92925), (59.871966, -141.444083), (59.970551, -141.46599), (59.948643, -141.706976), (60.019843, -141.964392), (60.085566, -142.539471), (60.091043, -142.873564), (60.036274, -143.623905), (59.997935, -143.892275), (60.140336, -144.231845), (60.206059, -144.65357), (60.29369, -144.785016), (60.441568, -144.834309), (60.430614, -145.124586), (60.299167, -145.223171), (60.474429, -145.738004), (60.551106, -145.820158), (60.408706, -146.351421), (60.238921, -146.608837), (60.397752, -146.718376), (60.485383, -146.608837), (60.463475, -146.455483), (60.578491, -145.951604), (60.666122, -146.017328), (60.622307, -146.252836), (60.737322, -146.345944), (60.753753, -146.565022), (61.044031, -146.784099), (60.972831, -146.866253), (60.934492, -147.172962), (60.972831, -147.271547), (60.879723, -147.375609), (60.912584, -147.758995), (60.808523, -147.775426), (60.781138, -148.032842), (60.819476, -148.153334), (61.005692, -148.065703), (61.000215, -148.175242), (60.803046, -148.350504), (60.737322, -148.109519), (60.594922, -148.087611), (60.441568, -147.939734), (60.277259, -148.027365), (60.332029, -148.219058), (60.249875, -148.273827), (60.217013, -148.087611), (59.997935, -147.983549), (59.95412, -148.251919), (59.997935, -148.399797), (59.937689, -148.635305), (59.986981, -148.755798), (59.981505, -149.067984), (60.063659, -149.05703), (60.008889, -149.204907), (59.904827, -149.287061), (59.997935, -149.418508), (59.866489, -149.582816), (59.806242, -149.511616), (59.729565, -149.741647), (59.718611, -149.949771), (59.61455, -150.031925), (59.521442, -150.25648), (59.554303, -150.409834), (59.444764, -150.579619), (59.450241, -150.716543), (59.225687, -151.001343), (59.209256, -151.308052), (59.280456, -151.406637), (59.159963, -151.592853), (59.253071, -151.976239), (59.422857, -151.888608), (59.483103, -151.636669), (59.472149, -151.47236), (59.537872, -151.423068), (59.669319, -151.127313), (59.778858, -151.116359), (59.63098, -151.505222), (59.718611, -151.828361), (59.778858, -151.8667), (60.030797, -151.702392), (60.211536, -151.423068), (60.359413, -151.379252), (60.386798, -151.297098), (60.545629, -151.264237), (60.720892, -151.406637), (60.786615, -151.06159), (61.038554, -150.404357), (60.939969, -150.245526), (60.912584, -150.042879), (61.016646, -149.741647), (61.15357, -150.075741), (61.257632, -150.207187), (61.246678, -150.47008), (61.29597, -150.656296), (61.252155, -150.711066), (61.180954, -151.023251), (61.044031, -151.165652), (61.011169, -151.477837), (60.852338, -151.800977), (60.748276, -151.833838), (60.693507, -152.080301), (60.578491, -152.13507), (60.507291, -152.310332), (60.304644, -152.392486), (60.173197, -152.732057), (60.069136, -152.567748), (59.915781, -152.704672), (59.888397, -153.022334), (59.691227, -153.049719), (59.620026, -153.345474), (59.702181, -153.438582), (59.548826, -153.586459), (59.543349, -153.761721), (59.433811, -153.72886), (59.368087, -154.117723), (59.066856, -154.1944), (59.050425, -153.750768), (58.968271, -153.400243), (58.869686, -153.301658), (58.710854, -153.444059), (58.612269, -153.679567), (58.606793, -153.898645), (58.519161, -153.920553), (58.4863, -154.062953), (58.376761, -153.99723), (58.212453, -154.145107), (58.059098, -154.46277), (58.059098, -154.643509), (58.004329, -154.818771), (58.015283, -154.988556), (57.955037, -155.120003), (57.872883, -155.081664), (57.829067, -155.328126), (57.708574, -155.377419), (57.785251, -155.547204), (57.549743, -155.73342), (57.566174, -156.045606), (57.440204, -156.023698), (57.473066, -156.209914), (57.418296, -156.34136), (57.248511, -156.34136), (56.985618, -156.549484), (56.952757, -156.883577), (56.832264, -157.157424), (56.766541, -157.20124), (56.859649, -157.376502), (56.607709, -157.672257), (56.67891, -157.754411), (56.657002, -157.918719), (56.514601, -157.957058), (56.459832, -158.126843), (56.48174, -158.32949), (56.339339, -158.488321), (56.295524, -158.208997), (55.977861, -158.510229), (55.873799, -159.375585), (55.594475, -159.616571), (55.654722, -159.676817), (55.829984, -159.643955), (55.857368, -159.813741), (55.791645, -160.027341), (55.720445, -160.060203), (55.605429, -160.394296), (55.473983, -160.536697), (55.567091, -160.580512), (55.457552, -160.668143), (55.528752, -160.865313), (55.358967, -161.232268), (55.364444, -161.506115), (55.49589, -161.467776), (55.62186, -161.588269), (55.517798, -161.697808), (55.408259, -161.686854), (55.074166, -162.053809), (55.15632, -162.179779), (55.03035, -162.218117), (55.052258, -162.470057), (55.249428, -162.508395), (55.293244, -162.661749), (55.222043, -162.716519), (55.134412, -162.579595), (54.997489, -162.645319), (54.926289, -162.847965), (55.079643, -163.00132), (55.090597, -163.187536), (55.03035, -163.220397), (54.942719, -163.034181), (54.800319, -163.373752), (54.76198, -163.14372), (54.696257, -163.138243), (54.74555, -163.329936), (54.614103, -163.587352), (54.61958, -164.085754), (54.531949, -164.332216), (54.466226, -164.354124), (54.389548, -164.638925), (54.416933, -164.847049), (54.603149, -164.918249), (54.663395, -164.710125), (54.88795, -164.551294), (54.893427, -164.34317), (55.041304, -163.894061), (55.046781, -163.532583), (54.904381, -163.39566), (55.008443, -163.291598), (55.128935, -163.313505), (55.183705, -163.105382), (55.183705, -162.880827), (55.446598, -162.579595), (55.682106, -162.245502), (55.89023, -161.807347), (55.983338, -161.292514), (55.939523, -161.078914), (55.999769, -160.87079), (55.912138, -160.816021), (55.813553, -160.931036), (55.736876, -160.805067), (55.857368, -160.766728), (55.868322, -160.509312), (55.791645, -160.438112), (55.76426, -160.27928), (55.857368, -160.273803), (55.939523, -160.536697), (55.994292, -160.558604), (56.251708, -160.383342), (56.399586, -160.147834), (56.541986, -159.830171), (56.667956, -159.326293), (56.848695, -158.959338), (56.782971, -158.784076), (56.810356, -158.641675), (56.925372, -158.701922), (57.034911, -158.658106), (57.264942, -158.378782), (57.41282, -157.995396), (57.609989, -157.688688), (57.719528, -157.705118), (58.497254, -157.458656), (58.705377, -157.07527), (58.869686, -157.119086), (58.634177, -158.039212), (58.661562, -158.32949), (58.760147, -158.40069), (58.803962, -158.564998), (58.913501, -158.619768), (58.864209, -158.767645), (58.694424, -158.860753), (58.480823, -158.701922), (58.387715, -158.893615), (58.420577, -159.0634), (58.760147, -159.392016), (58.929932, -159.616571), (58.929932, -159.731586), (58.803962, -159.808264), (58.782055, -159.906848), (58.886116, -160.054726), (58.902547, -160.235465), (59.072332, -160.317619), (58.88064, -160.854359), (58.743716, -161.33633), (58.667039, -161.374669), (58.552023, -161.752577), (58.656085, -161.938793), (58.776578, -161.769008), (59.061379, -161.829255), (59.36261, -161.955224), (59.48858, -161.703285), (59.740519, -161.911409), (59.88292, -162.092148), (60.091043, -162.234548), (60.178674, -162.448149), (59.997935, -162.502918), (59.959597, -162.760334), (59.844581, -163.171105), (59.795289, -163.66403), (59.806242, -163.9324), (59.866489, -164.162431), (60.02532, -164.189816), (60.074613, -164.386986), (60.29369, -164.699171), (60.337506, -164.962064), (60.578491, -165.268773), (60.68803, -165.060649), (60.890677, -165.016834), (60.846861, -165.175665), (60.972831, -165.197573), (61.076893, -165.120896), (61.170001, -165.323543), (61.071416, -165.34545), (61.109754, -165.591913), (61.279539, -165.624774), (61.301447, -165.816467), (61.416463, -165.920529), (61.558863, -165.915052), (61.49314, -166.106745), (61.630064, -166.139607), (61.662925, -165.904098), (61.81628, -166.095791), (61.827233, -165.756221), (62.013449, -165.756221), (62.139419, -165.674067), (62.539236, -165.044219), (62.659728, -164.912772), (62.637821, -164.819664), (62.807606, -164.874433), (63.097884, -164.633448), (63.212899, -164.425324), (63.262192, -164.036462), (63.212899, -163.73523), (63.037637, -163.313505), (63.059545, -163.039658), (63.22933, -162.661749), (63.486746, -162.272887), (63.514131, -162.075717), (63.448408, -162.026424), (63.448408, -161.555408), (63.503177, -161.13916), (63.771547, -160.766728), (63.837271, -160.766728), (64.08921, -160.952944), (64.237087, -160.974852), (64.395918, -161.26513), (64.532842, -161.374669), (64.494503, -161.078914), (64.609519, -160.79959), (64.719058, -160.783159), (64.921705, -161.144637), (64.762873, -161.413007), (64.790258, -161.664946), (64.702627, -161.900455), (64.680719, -162.168825), (64.620473, -162.234548), (64.532842, -162.541257), (64.384965, -162.634365), (64.324718, -162.787719), (64.49998, -162.858919), (64.538319, -163.045135), (64.401395, -163.176582), (64.467119, -163.253259), (64.565704, -163.598306), (64.560227, -164.304832), (64.450688, -164.80871), (64.434257, -165.000403), (64.49998, -165.411174), (64.576658, -166.188899), (64.636904, -166.391546), (64.735489, -166.484654), (64.872412, -166.413454), (64.987428, -166.692778), (65.113398, -166.638008), (65.179121, -166.462746), (65.337952, -166.517516), (65.337952, -166.796839), (65.381768, -167.026871), (65.414629, -167.47598), (65.496784, -167.711489), (65.578938, -168.072967), (65.682999, -168.105828), (65.819923, -167.541703), (66.049954, -166.829701), (66.186878, -166.3313), (66.110201, -166.046499), (66.09377, -165.756221), (66.203309, -165.690498), (66.21974, -165.86576), (66.312848, -165.88219), (66.466202, -165.186619), (66.581218, -164.403417), (66.592172, -163.981692), (66.553833, -163.751661), (66.389525, -163.872153), (66.274509, -163.828338), (66.192355, -163.915969), (66.060908, -163.768091), (66.082816, -163.494244), (66.060908, -163.149197), (66.088293, -162.749381), (66.039001, -162.634365), (66.028047, -162.371472), (66.077339, -162.14144), (66.02257, -161.840208), (66.241647, -161.549931), (66.252601, -161.341807), (66.208786, -161.199406), (66.334755, -161.128206), (66.395002, -161.528023), (66.345709, -161.911409), (66.510017, -161.87307), (66.68528, -162.174302), (66.740049, -162.502918), (66.89888, -162.601503), (66.937219, -162.344087), (66.778388, -162.015471), (66.652418, -162.075717), (66.553833, -161.916886), (66.438817, -161.571838), (66.55931, -161.489684), (66.718141, -161.884024), (67.002942, -161.714239), (67.052235, -161.851162), (66.991988, -162.240025), (67.008419, -162.639842), (67.057712, -162.700088), (67.008419, -162.902735), (67.128912, -163.740707), (67.254881, -163.757138), (67.534205, -164.009077), (67.638267, -164.211724), (67.725898, -164.534863), (67.966884, -165.192096), (68.059992, -165.493328), (68.081899, -165.794559), (68.246208, -166.243668), (68.339316, -166.681824), (68.372177, -166.703731), (68.42147, -166.375115), (68.574824, -166.227238), (68.881533, -166.216284), (68.859625, -165.329019), (68.930825, -164.255539), (68.985595, -163.976215), (69.138949, -163.532583), (69.374457, -163.110859), (69.609966, -163.023228), (69.812613, -162.842489), (69.982398, -162.470057), (70.108367, -162.311225), (70.311014, -161.851162), (70.256245, -161.779962), (70.239814, -161.396576), (70.343876, -160.837928), (70.453415, -160.487404), (70.792985, -159.649432), (70.809416, -159.33177), (70.760123, -159.298908), (70.798462, -158.975769), (70.787508, -158.658106), (70.831323, -158.033735), (70.979201, -157.420318), (71.285909, -156.812377), (71.351633, -156.565915), (71.296863, -156.522099), (71.170894, -155.585543), (71.083263, -155.508865), (70.968247, -155.832005), (70.96277, -155.979882), (70.809416, -155.974405), (70.858708, -155.503388), (70.940862, -155.476004), (71.017539, -155.262403), (70.973724, -155.191203), (71.148986, -155.032372), (70.990155, -154.566832), (70.869662, -154.643509), (70.8368, -154.353231), (70.7656, -154.183446), (70.880616, -153.931507), (70.886093, -153.487874), (70.924431, -153.235935), (70.886093, -152.589656), (70.842277, -152.26104), (70.606769, -152.419871), (70.546523, -151.817408), (70.486276, -151.773592), (70.382214, -151.187559), (70.431507, -151.182082), (70.49723, -150.760358), (70.491753, -150.355064), (70.436984, -150.349588), (70.431507, -150.114079), (70.508184, -149.867617), (70.519138, -149.462323), (70.486276, -149.177522), (70.404122, -148.78866), (70.420553, -148.607921), (70.305537, -148.350504), (70.349353, -148.202627), (70.316491, -147.961642), (70.245291, -147.786379)], [(58.026237, -152.94018), (57.982421, -152.945657), (58.048145, -153.290705), (58.305561, -153.044242), (58.327469, -152.819688), (58.562977, -152.666333), (58.354853, -152.496548), (58.426053, -152.354148), (58.311038, -152.080301), (58.152206, -152.080301), (58.130299, -152.480117), (58.059098, -152.655379), (58.026237, -152.94018)], [(57.538789, -153.958891), (57.670236, -153.67409), (57.69762, -153.931507), (57.812636, -153.936983), (57.889313, -153.723383), (57.834544, -153.570028), (57.719528, -153.548121), (57.796205, -153.46049), (57.96599, -153.455013), (57.889313, -153.268797), (57.998852, -153.235935), (57.933129, -153.071627), (57.933129, -152.874457), (57.993375, -152.721103), (57.889313, -152.469163), (57.599035, -152.469163), (57.620943, -152.151501), (57.42925, -152.359625), (57.505928, -152.74301), (57.379958, -152.60061), (57.275896, -152.710149), (57.325188, -152.907319), (57.128019, -152.912796), (57.073249, -153.214027), (56.991095, -153.312612), (57.067772, -153.498828), (56.859649, -153.695998), (56.837741, -153.849352), (56.744633, -154.013661), (56.969187, -154.073907), (56.848695, -154.303938), (56.919895, -154.314892), (56.991095, -154.523016), (57.193742, -154.539447), (57.275896, -154.742094), (57.511404, -154.627078), (57.659282, -154.227261), (57.648328, -153.980799), (57.538789, -153.958891)], [(56.602232, -154.53397), (56.399586, -154.742094), (56.432447, -154.807817), (56.602232, -154.53397)], [(55.923092, -155.634835), (55.912138, -155.476004), (55.704014, -155.530773), (55.731399, -155.793666), (55.802599, -155.837482), (55.923092, -155.634835)], [(55.28229, -159.890418), (55.068689, -159.950664), (54.893427, -160.257373), (55.161797, -160.109495), (55.134412, -160.005433), (55.28229, -159.890418)], [(55.358967, -160.520266), (55.358967, -160.33405), (55.249428, -160.339527), (55.128935, -160.525743), (55.211089, -160.690051), (55.134412, -160.794113), (55.320628, -160.854359), (55.380875, -160.79959), (55.358967, -160.520266)], [(54.981058, -162.256456), (54.893427, -162.234548), (54.838658, -162.349564), (54.931766, -162.437195), (54.981058, -162.256456)], [(63.634624, -162.415287), (63.536039, -162.563165), (63.62367, -162.612457), (63.634624, -162.415287)], [(54.488133, -162.80415), (54.449795, -162.590549), (54.367641, -162.612457), (54.373118, -162.782242), (54.488133, -162.80415)], [(54.29644, -165.548097), (54.181425, -165.476897), (54.132132, -165.630251), (54.252625, -165.685021), (54.29644, -165.548097)], [(54.15404, -165.73979), (54.044501, -166.046499), (54.121178, -166.112222), (54.219763, -165.980775), (54.15404, -165.73979)], [(60.359413, -166.364161), (60.397752, -166.13413), (60.326552, -166.084837), (60.342983, -165.88219), (60.277259, -165.685021), (59.992458, -165.646682), (59.89935, -165.750744), (59.844581, -166.00816), (59.745996, -166.062929), (59.855535, -166.440838), (59.850058, -166.6161), (59.992458, -166.994009), (59.992458, -167.125456), (60.074613, -167.344534), (60.206059, -167.421211), (60.238921, -167.311672), (60.206059, -166.93924), (60.310121, -166.763978), (60.321075, -166.577762), (60.392275, -166.495608), (60.359413, -166.364161)], [(54.01164, -166.375115), (53.934962, -166.210807), (53.748746, -166.5449), (53.715885, -166.539423), (53.852808, -166.117699), (53.776131, -166.112222), (53.683023, -166.282007), (53.622777, -166.555854), (53.529669, -166.583239), (53.431084, -166.878994), (53.425607, -167.13641), (53.332499, -167.306195), (53.250345, -167.623857), (53.337976, -167.793643), (53.442038, -167.459549), (53.425607, -167.355487), (53.513238, -167.103548), (53.611823, -167.163794), (53.715885, -167.021394), (53.666592, -166.807793), (53.732316, -166.785886), (53.754223, -167.015917), (53.825424, -167.141887), (53.945916, -167.032348), (54.017116, -166.643485), (53.880193, -166.561331), (54.01164, -166.375115)], [(53.157237, -168.790446), (53.34893, -168.40706), (53.431084, -168.385152), (53.524192, -168.237275), (53.568007, -168.007243), (53.518715, -167.886751), (53.387268, -167.842935), (53.244868, -168.270136), (53.036744, -168.500168), (52.965544, -168.686384), (53.157237, -168.790446)], [(52.894344, -169.74891), (52.795759, -169.705095), (52.790282, -169.962511), (52.856005, -169.989896), (52.894344, -169.74891)], [(57.221127, -170.148727), (57.128019, -170.28565), (57.221127, -170.313035), (57.221127, -170.148727)], [(52.697174, -170.669036), (52.604066, -170.603313), (52.538343, -170.789529), (52.636928, -170.816914), (52.697174, -170.669036)], [(63.716778, -171.742517), (63.5689, -170.94836), (63.69487, -170.488297), (63.683916, -170.280174), (63.612716, -170.093958), (63.492223, -170.044665), (63.4265, -169.644848), (63.366254, -169.518879), (63.338869, -168.99857), (63.295053, -168.686384), (63.147176, -168.856169), (63.180038, -169.108108), (63.152653, -169.376478), (63.08693, -169.513402), (62.939052, -169.639372), (63.075976, -169.831064), (63.169084, -170.055619), (63.180038, -170.263743), (63.2841, -170.362328), (63.415546, -170.866206), (63.421023, -171.101715), (63.306007, -171.463193), (63.366254, -171.73704), (63.486746, -171.852055), (63.716778, -171.742517)], [(52.390465, -172.432611), (52.275449, -172.41618), (52.253542, -172.607873), (52.352127, -172.569535), (52.390465, -172.432611)], [(52.14948, -173.626584), (52.105664, -173.495138), (52.111141, -173.122706), (52.07828, -173.106275), (52.028987, -173.549907), (52.14948, -173.626584)], [(52.280926, -174.322156), (52.379511, -174.327632), (52.41785, -174.185232), (52.319265, -173.982585), (52.226157, -174.059262), (52.231634, -174.179755), (52.127572, -174.141417), (52.116618, -174.333109), (52.007079, -174.738403), (52.039941, -174.968435), (52.116618, -174.902711), (52.105664, -174.656249), (52.280926, -174.322156)], [(51.853725, -176.469116), (51.870156, -176.288377), (51.744186, -176.288377), (51.760617, -176.518409), (51.61274, -176.80321), (51.80991, -176.912748), (51.815386, -176.792256), (51.963264, -176.775825), (51.968741, -176.627947), (51.859202, -176.627947), (51.853725, -176.469116)], [(51.946833, -177.153734), (51.897541, -177.044195), (51.727755, -177.120872), (51.678463, -177.274226), (51.782525, -177.279703), (51.946833, -177.153734)], [(51.919448, -178.123152), (51.913971, -177.953367), (51.793479, -177.800013), (51.651078, -177.964321), (51.919448, -178.123152)], [(52.992929, 173.107557), (52.927205, 173.293773), (52.823143, 173.304726), (52.762897, 172.90491), (52.927205, 172.642017), (53.003883, 172.642017), (52.992929, 173.107557)]], 'NV': [[(42.000709, -117.027882), (41.995232, -114.04295), (37.000263, -114.048427), (36.195153, -114.048427), (36.025367, -114.152489), (36.01989, -114.251074), (36.140383, -114.371566), (36.102045, -114.738521), (35.516012, -114.678275), (35.324319, -114.596121), (35.138103, -114.574213), (35.00118, -114.634459), (35.970598, -115.85034), (36.501861, -116.540435), (37.21934, -117.498899), (38.101128, -118.71478), (38.999346, -120.001861), (40.264519, -119.996384), (41.995232, -120.001861), (41.989755, -118.698349), (42.000709, -117.027882)]], 'VA': [[(38.013497, -75.397659), (38.029928, -75.244304), (37.860142, -75.375751), (37.799896, -75.512674), (37.569865, -75.594828), (37.197433, -75.802952), (37.120755, -75.972737), (37.257679, -76.027507), (37.564388, -75.939876), (37.95325, -75.671506), (38.013497, -75.397659)], [(37.95325, -76.016553), (37.95325, -75.994645), (37.95325, -76.043938), (37.95325, -76.016553)], [(39.464886, -78.349729), (39.130793, -77.82942), (39.322485, -77.719881), (39.306055, -77.566527), (39.223901, -77.456988), (39.076023, -77.456988), (39.026731, -77.248864), (38.933623, -77.117418), (38.791222, -77.040741), (38.632391, -77.128372), (38.588575, -77.248864), (38.446175, -77.325542), (38.342113, -77.281726), (38.374975, -77.013356), (38.216144, -76.964064), (38.15042, -76.613539), (38.024451, -76.514954), (37.887527, -76.235631), (37.608203, -76.3616), (37.389126, -76.246584), (37.285064, -76.383508), (37.159094, -76.399939), (37.082417, -76.273969), (36.961924, -76.410893), (37.120755, -76.619016), (37.065986, -76.668309), (36.95097, -76.48757), (36.923586, -75.994645), (36.551154, -75.868676), (36.5402, -79.510841), (36.545677, -80.294043), (36.562108, -80.978661), (36.589492, -81.679709), (36.600446, -83.673316), (36.742847, -83.136575), (36.852385, -83.070852), (36.890724, -82.879159), (36.978355, -82.868205), (37.044078, -82.720328), (37.120755, -82.720328), (37.268633, -82.353373), (37.537003, -81.969987), (37.454849, -81.986418), (37.285064, -81.849494), (37.20291, -81.679709), (37.208387, -81.55374), (37.339833, -81.362047), (37.235771, -81.225123), (37.290541, -80.967707), (37.482234, -80.513121), (37.421987, -80.474782), (37.509618, -80.29952), (37.690357, -80.294043), (37.849189, -80.184505), (37.997066, -79.998289), (38.177805, -79.921611), (38.364021, -79.724442), (38.594052, -79.647764), (38.457129, -79.477979), (38.413313, -79.313671), (38.495467, -79.209609), (38.851469, -78.996008), (38.763838, -78.870039), (39.169131, -78.404499), (39.464886, -78.349729)]], 'CO': [[(41.003906, -107.919731), (40.998429, -105.728954), (41.003906, -104.053011), (41.003906, -102.053927), (40.001626, -102.053927), (36.994786, -102.042974), (37.000263, -103.001438), (36.994786, -104.337812), (36.994786, -106.868158), (37.000263, -107.421329), (37.000263, -109.042503), (38.166851, -109.042503), (38.27639, -109.058934), (39.125316, -109.053457), (40.998429, -109.04798), (41.003906, -107.919731)]], 'CA': [[(42.006186, -123.233256), (42.011663, -122.378853), (41.995232, -121.037003), (41.995232, -120.001861), (40.264519, -119.996384), (38.999346, -120.001861), (38.101128, -118.71478), (37.21934, -117.498899), (36.501861, -116.540435), (35.970598, -115.85034), (35.00118, -114.634459), (34.87521, -114.634459), (34.710902, -114.470151), (34.448009, -114.333228), (34.305608, -114.136058), (34.174162, -114.256551), (34.108438, -114.415382), (33.933176, -114.535874), (33.697668, -114.497536), (33.54979, -114.524921), (33.40739, -114.727567), (33.034958, -114.661844), (33.029481, -114.524921), (32.843265, -114.470151), (32.755634, -114.524921), (32.717295, -114.72209), (32.624187, -116.04751), (32.536556, -117.126467), (32.668003, -117.24696), (32.876127, -117.252437), (33.122589, -117.329114), (33.297851, -117.471515), (33.538836, -117.7837), (33.763391, -118.183517), (33.703145, -118.260194), (33.741483, -118.413548), (33.840068, -118.391641), (34.042715, -118.566903), (33.998899, -118.802411), (34.146777, -119.218659), (34.26727, -119.278905), (34.415147, -119.558229), (34.40967, -119.875891), (34.475393, -120.138784), (34.448009, -120.472878), (34.579455, -120.64814), (34.858779, -120.609801), (34.902595, -120.670048), (35.099764, -120.631709), (35.247642, -120.894602), (35.450289, -120.905556), (35.461243, -121.004141), (35.636505, -121.168449), (35.674843, -121.283465), (35.784382, -121.332757), (36.195153, -121.716143), (36.315645, -121.896882), (36.638785, -121.935221), (36.6114, -121.858544), (36.803093, -121.787344), (36.978355, -121.929744), (36.956447, -122.105006), (37.115279, -122.335038), (37.241248, -122.417192), (37.361741, -122.400761), (37.520572, -122.515777), (37.783465, -122.515777), (37.783465, -122.329561), (38.15042, -122.406238), (38.112082, -122.488392), (37.931343, -122.504823), (37.893004, -122.701993), (38.029928, -122.937501), (38.265436, -122.97584), (38.451652, -123.129194), (38.566668, -123.331841), (38.698114, -123.44138), (38.95553, -123.737134), (39.032208, -123.687842), (39.366301, -123.824765), (39.552517, -123.764519), (39.831841, -123.85215), (40.105688, -124.109566), (40.259042, -124.361506), (40.439781, -124.410798), (40.877937, -124.158859), (41.025814, -124.109566), (41.14083, -124.158859), (41.442061, -124.065751), (41.715908, -124.147905), (41.781632, -124.257444), (42.000709, -124.213628), (42.006186, -123.233256)]], 'AL': [[(35.00118, -87.359296), (34.984749, -85.606675), (34.124869, -85.431413), (32.859696, -85.184951), (32.580372, -85.069935), (32.421541, -84.960397), (32.322956, -85.004212), (32.262709, -84.889196), (32.13674, -85.058981), (32.01077, -85.053504), (31.840985, -85.141136), (31.539753, -85.042551), (31.27686, -85.113751), (31.003013, -85.004212), (30.997536, -85.497137), (30.997536, -87.600282), (30.86609, -87.633143), (30.674397, -87.408589), (30.510088, -87.446927), (30.427934, -87.37025), (30.280057, -87.518128), (30.247195, -87.655051), (30.411504, -87.90699), (30.657966, -87.934375), (30.685351, -88.011052), (30.499135, -88.10416), (30.318396, -88.137022), (30.367688, -88.394438), (31.895754, -88.471115), (33.796253, -88.241084), (34.891641, -88.098683), (34.995703, -88.202745), (35.00118, -87.359296)]], 'AR': [[(36.501861, -94.473842), (36.496384, -90.152536), (36.304691, -90.064905), (36.184199, -90.218259), (35.997983, -90.377091), (35.997983, -89.730812), (35.811767, -89.763673), (35.756997, -89.911551), (35.603643, -89.944412), (35.439335, -90.130628), (35.198349, -90.114197), (35.023087, -90.212782), (34.995703, -90.311367), (34.908072, -90.251121), (34.831394, -90.409952), (34.661609, -90.481152), (34.617794, -90.585214), (34.420624, -90.568783), (34.365854, -90.749522), (34.300131, -90.744046), (34.135823, -90.952169), (34.026284, -90.891923), (33.867453, -91.072662), (33.560744, -91.231493), (33.429298, -91.056231), (33.347144, -91.143862), (33.13902, -91.089093), (33.002096, -91.16577), (33.018527, -93.608485), (33.018527, -94.041164), (33.54979, -94.041164), (33.593606, -94.183564), (33.544313, -94.380734), (33.637421, -94.484796), (35.395519, -94.430026), (36.501861, -94.616242), (36.501861, -94.473842)]], 'VT': [[(45.013027, -71.503554), (44.914442, -71.4926), (44.750133, -71.629524), (44.585825, -71.536416), (44.41604, -71.700724), (44.322932, -72.034817), (44.07647, -72.02934), (43.994316, -72.116971), (43.769761, -72.204602), (43.572591, -72.379864), (43.150867, -72.456542), (43.008466, -72.445588), (42.953697, -72.533219), (42.80582, -72.544173), (42.729142, -72.456542), (42.745573, -73.267129), (42.833204, -73.278083), (43.523299, -73.245221), (43.687607, -73.404052), (43.769761, -73.349283), (44.043608, -73.436914), (44.246255, -73.321898), (44.437948, -73.294514), (44.618687, -73.387622), (44.804903, -73.332852), (45.013027, -73.343806), (45.002073, -72.308664), (45.013027, -71.503554)]], 'IL': [[(42.510065, -90.639984), (42.493634, -88.788778), (42.493634, -87.802929), (42.301941, -87.83579), (42.077386, -87.682436), (41.710431, -87.523605), (39.34987, -87.529082), (39.169131, -87.63862), (38.95553, -87.512651), (38.780268, -87.49622), (38.637868, -87.62219), (38.506421, -87.655051), (38.292821, -87.83579), (38.27639, -87.950806), (38.15042, -87.923421), (38.101128, -88.000098), (37.865619, -88.060345), (37.799896, -88.027483), (37.657496, -88.15893), (37.482234, -88.065822), (37.389126, -88.476592), (37.285064, -88.514931), (37.153617, -88.421823), (37.071463, -88.547792), (37.224817, -88.914747), (37.213863, -89.029763), (37.038601, -89.183118), (36.983832, -89.133825), (36.994786, -89.292656), (37.279587, -89.517211), (37.34531, -89.435057), (37.537003, -89.517211), (37.690357, -89.517211), (37.903958, -89.84035), (37.88205, -89.949889), (38.013497, -90.059428), (38.216144, -90.355183), (38.374975, -90.349706), (38.632391, -90.179921), (38.725499, -90.207305), (38.845992, -90.10872), (38.917192, -90.251121), (38.961007, -90.470199), (38.867899, -90.585214), (38.928146, -90.661891), (39.256762, -90.727615), (39.470363, -91.061708), (39.727779, -91.368417), (40.034488, -91.494386), (40.237135, -91.50534), (40.379535, -91.417709), (40.560274, -91.401278), (40.669813, -91.121954), (40.823167, -91.09457), (40.921752, -90.963123), (41.097014, -90.946692), (41.239415, -91.111001), (41.414677, -91.045277), (41.463969, -90.656414), (41.589939, -90.344229), (41.743293, -90.311367), (41.809016, -90.179921), (42.000709, -90.141582), (42.126679, -90.168967), (42.225264, -90.393521), (42.329326, -90.420906), (42.510065, -90.639984)]], 'GA': [[(35.00118, -83.109191), (34.787579, -83.322791), (34.683517, -83.339222), (34.469916, -83.005129), (34.486347, -82.901067), (34.26727, -82.747713), (34.152254, -82.714851), (33.94413, -82.55602), (33.81816, -82.325988), (33.631944, -82.194542), (33.462159, -81.926172), (33.347144, -81.937125), (33.160928, -81.761863), (33.007573, -81.493493), (32.843265, -81.42777), (32.629664, -81.416816), (32.558464, -81.279893), (32.290094, -81.121061), (32.120309, -81.115584), (32.032678, -80.885553), (31.693108, -81.132015), (31.517845, -81.175831), (31.364491, -81.279893), (31.20566, -81.290846), (31.13446, -81.400385), (30.707258, -81.444201), (30.745597, -81.718048), (30.827751, -81.948079), (30.751074, -82.041187), (30.564858, -82.002849), (30.362211, -82.046664), (30.356734, -82.167157), (30.570335, -82.216449), (30.647012, -83.498053), (30.712735, -84.867289), (31.003013, -85.004212), (31.27686, -85.113751), (31.539753, -85.042551), (31.840985, -85.141136), (32.01077, -85.053504), (32.13674, -85.058981), (32.262709, -84.889196), (32.322956, -85.004212), (32.421541, -84.960397), (32.580372, -85.069935), (32.859696, -85.184951), (34.124869, -85.431413), (34.984749, -85.606675), (34.990226, -84.319594), (34.984749, -83.618546), (35.00118, -83.109191)]], 'IN': [[(41.759724, -85.990061), (41.759724, -84.807042), (41.694001, -84.807042), (40.500028, -84.801565), (39.103408, -84.817996), (39.059592, -84.894673), (38.785745, -84.812519), (38.780268, -84.987781), (38.68716, -85.173997), (38.730976, -85.431413), (38.533806, -85.42046), (38.451652, -85.590245), (38.325682, -85.655968), (38.27639, -85.83123), (38.024451, -85.924338), (37.958727, -86.039354), (38.051835, -86.263908), (38.166851, -86.302247), (38.040881, -86.521325), (37.931343, -86.504894), (37.893004, -86.729448), (37.991589, -86.795172), (37.893004, -87.047111), (37.788942, -87.129265), (37.93682, -87.381204), (37.903958, -87.512651), (37.975158, -87.600282), (37.903958, -87.682436), (37.893004, -87.934375), (37.799896, -88.027483), (37.865619, -88.060345), (38.101128, -88.000098), (38.15042, -87.923421), (38.27639, -87.950806), (38.292821, -87.83579), (38.506421, -87.655051), (38.637868, -87.62219), (38.780268, -87.49622), (38.95553, -87.512651), (39.169131, -87.63862), (39.34987, -87.529082), (41.710431, -87.523605), (41.644708, -87.42502), (41.644708, -87.118311), (41.759724, -86.822556), (41.759724, -85.990061)]], 'IA': [[(43.501391, -91.368417), (43.501391, -91.215062), (43.353514, -91.204109), (43.254929, -91.056231), (43.134436, -91.176724), (42.909881, -91.143862), (42.75105, -91.067185), (42.636034, -90.711184), (42.510065, -90.639984), (42.329326, -90.420906), (42.225264, -90.393521), (42.126679, -90.168967), (42.000709, -90.141582), (41.809016, -90.179921), (41.743293, -90.311367), (41.589939, -90.344229), (41.463969, -90.656414), (41.414677, -91.045277), (41.239415, -91.111001), (41.097014, -90.946692), (40.921752, -90.963123), (40.823167, -91.09457), (40.669813, -91.121954), (40.560274, -91.401278), (40.379535, -91.417709), (40.412397, -91.527248), (40.615043, -91.729895), (40.609566, -91.833957), (40.582182, -93.257961), (40.571228, -94.632673), (40.587659, -95.7664), (40.719105, -95.881416), (40.976521, -95.826646), (41.201076, -95.925231), (41.453015, -95.919754), (41.540646, -96.095016), (41.67757, -96.122401), (41.798063, -96.062155), (41.973325, -96.127878), (42.039048, -96.264801), (42.488157, -96.44554), (42.707235, -96.631756), (42.855112, -96.544125), (43.052282, -96.511264), (43.123482, -96.434587), (43.222067, -96.560556), (43.397329, -96.527695), (43.479483, -96.582464), (43.501391, -96.451017), (43.501391, -91.368417)]], 'MA': [[(42.887974, -70.917521), (42.871543, -70.818936), (42.696281, -70.780598), (42.55388, -70.824413), (42.422434, -70.983245), (42.269079, -70.988722), (42.247172, -70.769644), (42.08834, -70.638197), (41.962371, -70.660105), (41.929509, -70.550566), (41.814493, -70.539613), (41.715908, -70.260289), (41.809016, -69.937149), (41.672093, -70.008349), (41.5516, -70.484843), (41.546123, -70.660105), (41.639231, -70.764167), (41.611847, -70.928475), (41.540646, -70.933952), (41.496831, -71.120168), (41.67757, -71.196845), (41.710431, -71.22423), (41.781632, -71.328292), (42.01714, -71.383061), (42.01714, -71.530939), (42.006186, -71.799309), (42.022617, -71.799309), (42.039048, -73.053528), (42.050002, -73.486206), (42.08834, -73.508114), (42.745573, -73.267129), (42.729142, -72.456542), (42.696281, -71.29543), (42.789389, -71.185891), (42.887974, -70.917521)]], 'AZ': [[(37.000263, -109.042503), (31.331629, -109.04798), (31.331629, -111.074448), (31.704061, -112.246513), (32.492741, -114.815198), (32.717295, -114.72209), (32.755634, -114.524921), (32.843265, -114.470151), (33.029481, -114.524921), (33.034958, -114.661844), (33.40739, -114.727567), (33.54979, -114.524921), (33.697668, -114.497536), (33.933176, -114.535874), (34.108438, -114.415382), (34.174162, -114.256551), (34.305608, -114.136058), (34.448009, -114.333228), (34.710902, -114.470151), (34.87521, -114.634459), (35.00118, -114.634459), (35.138103, -114.574213), (35.324319, -114.596121), (35.516012, -114.678275), (36.102045, -114.738521), (36.140383, -114.371566), (36.01989, -114.251074), (36.025367, -114.152489), (36.195153, -114.048427), (37.000263, -114.048427), (37.00574, -110.499369), (37.000263, -109.042503)]], 'ID': [[(49.000239, -116.04751), (47.976051, -116.04751), (47.696727, -115.724371), (47.42288, -115.718894), (47.302388, -115.527201), (47.258572, -115.324554), (47.187372, -115.302646), (46.919002, -114.930214), (46.809463, -114.886399), (46.705401, -114.623506), (46.639678, -114.612552), (46.645155, -114.322274), (46.272723, -114.464674), (46.037214, -114.492059), (45.88386, -114.387997), (45.774321, -114.568736), (45.670259, -114.497536), (45.560721, -114.546828), (45.456659, -114.333228), (45.593582, -114.086765), (45.703121, -113.98818), (45.604536, -113.807441), (45.522382, -113.834826), (45.330689, -113.736241), (45.128042, -113.571933), (45.056842, -113.45144), (44.865149, -113.456917), (44.782995, -113.341901), (44.772041, -113.133778), (44.448902, -113.002331), (44.394132, -112.887315), (44.48724, -112.783254), (44.481763, -112.471068), (44.569394, -112.241036), (44.520102, -112.104113), (44.563917, -111.868605), (44.509148, -111.819312), (44.547487, -111.616665), (44.75561, -111.386634), (44.580348, -111.227803), (44.476286, -111.047063), (42.000709, -111.047063), (41.995232, -112.164359), (41.995232, -114.04295), (42.000709, -117.027882), (43.830007, -117.027882), (44.158624, -116.896436), (44.240778, -116.97859), (44.257209, -117.170283), (44.394132, -117.241483), (44.750133, -117.038836), (44.782995, -116.934774), (44.930872, -116.830713), (45.02398, -116.847143), (45.144473, -116.732128), (45.319735, -116.671881), (45.61549, -116.463758), (45.752413, -116.545912), (45.823614, -116.78142), (45.993399, -116.918344), (46.168661, -116.92382), (46.343923, -117.055267), (46.426077, -117.038836), (47.762451, -117.044313), (49.000239, -117.033359), (49.000239, -116.04751)]], 'CT': [[(42.039048, -73.053528), (42.022617, -71.799309), (42.006186, -71.799309), (41.414677, -71.799309), (41.321569, -71.859555), (41.338, -71.947186), (41.261322, -72.385341), (41.28323, -72.905651), (41.146307, -73.130205), (41.102491, -73.371191), (40.987475, -73.655992), (41.102491, -73.727192), (41.21203, -73.48073), (41.294184, -73.55193), (42.050002, -73.486206), (42.039048, -73.053528)]], 'ME': [[(43.057759, -70.703921), (43.128959, -70.824413), (43.227544, -70.807983), (43.34256, -70.966814), (44.657025, -71.032537), (45.303304, -71.08183), (45.440228, -70.649151), (45.511428, -70.720352), (45.664782, -70.556043), (45.735983, -70.386258), (45.796229, -70.41912), (45.889337, -70.260289), (46.064599, -70.309581), (46.327492, -70.210996), (46.415123, -70.057642), (46.694447, -69.997395), (47.461219, -69.225147), (47.428357, -69.044408), (47.242141, -69.033454), (47.176418, -68.902007), (47.285957, -68.578868), (47.285957, -68.376221), (47.357157, -68.233821), (47.198326, -67.954497), (47.066879, -67.790188), (45.944106, -67.779235), (45.675736, -67.801142), (45.604536, -67.456095), (45.48952, -67.505388), (45.379982, -67.417757), (45.281397, -67.488957), (45.128042, -67.346556), (45.160904, -67.16034), (44.804903, -66.979601), (44.646072, -67.187725), (44.706318, -67.308218), (44.596779, -67.406803), (44.624164, -67.549203), (44.531056, -67.565634), (44.54201, -67.75185), (44.328409, -68.047605), (44.476286, -68.118805), (44.48724, -68.222867), (44.328409, -68.173574), (44.251732, -68.403606), (44.377701, -68.458375), (44.311978, -68.567914), (44.311978, -68.82533), (44.459856, -68.830807), (44.426994, -68.984161), (44.322932, -68.956777), (44.103854, -69.099177), (44.043608, -69.071793), (43.923115, -69.258008), (43.966931, -69.444224), (43.840961, -69.553763), (43.82453, -69.707118), (43.720469, -69.833087), (43.742376, -69.986442), (43.851915, -70.030257), (43.676653, -70.254812), (43.567114, -70.194565), (43.528776, -70.358873), (43.435668, -70.369827), (43.320652, -70.556043), (43.057759, -70.703921)]], 'MD': [[(37.95325, -75.994645), (37.95325, -76.016553), (37.95325, -76.043938), (37.95325, -75.994645)], [(39.722302, -79.477979), (39.722302, -75.786521), (38.462606, -75.693413), (38.451652, -75.047134), (38.029928, -75.244304), (38.013497, -75.397659), (37.95325, -75.671506), (37.909435, -75.885106), (38.073743, -75.879629), (38.139466, -75.961783), (38.210667, -75.846768), (38.374975, -76.000122), (38.303775, -76.049415), (38.320205, -76.257538), (38.500944, -76.328738), (38.500944, -76.263015), (38.736453, -76.257538), (38.829561, -76.191815), (39.147223, -76.279446), (39.333439, -76.169907), (39.366301, -76.000122), (39.557994, -75.972737), (39.536086, -76.098707), (39.437501, -76.104184), (39.311532, -76.367077), (39.196516, -76.443754), (38.906238, -76.460185), (38.769315, -76.55877), (38.539283, -76.514954), (38.380452, -76.383508), (38.259959, -76.399939), (38.139466, -76.317785), (38.057312, -76.3616), (38.216144, -76.591632), (38.292821, -76.920248), (38.446175, -77.018833), (38.358544, -77.205049), (38.479037, -77.276249), (38.632391, -77.128372), (38.791222, -77.040741), (38.895284, -76.909294), (38.993869, -77.035264), (38.933623, -77.117418), (39.026731, -77.248864), (39.076023, -77.456988), (39.223901, -77.456988), (39.306055, -77.566527), (39.322485, -77.719881), (39.601809, -77.834897), (39.601809, -78.004682), (39.694917, -78.174467), (39.61824, -78.267575), (39.623717, -78.431884), (39.514178, -78.470222), (39.585379, -78.765977), (39.437501, -78.963147), (39.470363, -79.094593), (39.300578, -79.291763), (39.20747, -79.488933), (39.722302, -79.477979)]], 'OK': [[(37.000263, -100.087706), (37.000263, -94.616242), (36.501861, -94.616242), (35.395519, -94.430026), (33.637421, -94.484796), (33.74696, -94.868182), (33.861976, -94.966767), (33.960561, -95.224183), (33.87293, -95.289906), (33.878407, -95.547322), (33.933176, -95.602092), (33.834591, -95.8376), (33.889361, -95.936185), (33.840068, -96.149786), (33.686714, -96.346956), (33.774345, -96.423633), (33.845545, -96.631756), (33.845545, -96.850834), (33.960561, -96.922034), (33.736006, -97.173974), (33.861976, -97.256128), (33.823637, -97.371143), (33.905791, -97.458774), (33.982469, -97.694283), (33.851022, -97.869545), (33.987946, -97.946222), (34.004376, -98.088623), (34.113915, -98.170777), (34.157731, -98.36247), (34.064623, -98.488439), (34.146777, -98.570593), (34.135823, -98.767763), (34.223454, -98.986841), (34.2125, -99.189488), (34.404193, -99.260688), (34.415147, -99.57835), (34.382285, -99.698843), (34.573978, -99.923398), (34.563024, -100.000075), (36.501861, -100.000075), (36.501861, -101.812942), (36.501861, -103.001438), (37.000263, -103.001438), (36.994786, -102.042974), (37.000263, -100.087706)]], 'OH': [[(41.978802, -80.518598), (40.636951, -80.518598), (40.582182, -80.666475), (40.472643, -80.595275), (40.319289, -80.600752), (40.078303, -80.737675), (39.711348, -80.830783), (39.388209, -81.219646), (39.344393, -81.345616), (39.410117, -81.455155), (39.267716, -81.57017), (39.273193, -81.685186), (39.0815, -81.811156), (38.966484, -81.783771), (38.873376, -81.887833), (39.026731, -82.03571), (38.785745, -82.221926), (38.632391, -82.172634), (38.577622, -82.293127), (38.446175, -82.331465), (38.424267, -82.594358), (38.561191, -82.731282), (38.588575, -82.846298), (38.758361, -82.890113), (38.725499, -83.032514), (38.626914, -83.142052), (38.703591, -83.519961), (38.632391, -83.678792), (38.769315, -83.903347), (38.807653, -84.215533), (38.895284, -84.231963), (39.103408, -84.43461), (39.103408, -84.817996), (40.500028, -84.801565), (41.694001, -84.807042), (41.732339, -83.454238), (41.595416, -83.065375), (41.513262, -82.933929), (41.589939, -82.835344), (41.431108, -82.616266), (41.381815, -82.479343), (41.513262, -82.013803), (41.485877, -81.739956), (41.672093, -81.444201), (41.852832, -81.011523), (41.978802, -80.518598), (41.978802, -80.518598)]], 'UT': [[(41.995232, -112.164359), (42.000709, -111.047063), (40.998429, -111.047063), (40.998429, -109.04798), (39.125316, -109.053457), (38.27639, -109.058934), (38.166851, -109.042503), (37.000263, -109.042503), (37.00574, -110.499369), (37.000263, -114.048427), (41.995232, -114.04295), (41.995232, -112.164359)]], 'MO': [[(40.609566, -91.833957), (40.615043, -91.729895), (40.412397, -91.527248), (40.379535, -91.417709), (40.237135, -91.50534), (40.034488, -91.494386), (39.727779, -91.368417), (39.470363, -91.061708), (39.256762, -90.727615), (38.928146, -90.661891), (38.867899, -90.585214), (38.961007, -90.470199), (38.917192, -90.251121), (38.845992, -90.10872), (38.725499, -90.207305), (38.632391, -90.179921), (38.374975, -90.349706), (38.216144, -90.355183), (38.013497, -90.059428), (37.88205, -89.949889), (37.903958, -89.84035), (37.690357, -89.517211), (37.537003, -89.517211), (37.34531, -89.435057), (37.279587, -89.517211), (36.994786, -89.292656), (36.983832, -89.133825), (36.578538, -89.215979), (36.622354, -89.363857), (36.496384, -89.418626), (36.496384, -89.484349), (36.496384, -89.539119), (36.249922, -89.533642), (35.997983, -89.730812), (35.997983, -90.377091), (36.184199, -90.218259), (36.304691, -90.064905), (36.496384, -90.152536), (36.501861, -94.473842), (36.501861, -94.616242), (37.000263, -94.616242), (39.158177, -94.610765), (39.20747, -94.824366), (39.442978, -94.983197), (39.541563, -95.109167), (39.831841, -94.884612), (39.908518, -95.207752), (40.001626, -95.306337), (40.264519, -95.552799), (40.587659, -95.7664), (40.571228, -94.632673), (40.582182, -93.257961), (40.609566, -91.833957)]], 'MN': [[(46.705401, -92.014696), (46.749217, -92.091373), (46.667063, -92.29402), (46.075553, -92.29402), (46.015307, -92.354266), (45.933153, -92.639067), (45.719552, -92.869098), (45.577151, -92.885529), (45.566198, -92.770513), (45.440228, -92.644544), (45.286874, -92.75956), (45.117088, -92.737652), (44.750133, -92.808852), (44.569394, -92.545959), (44.552964, -92.337835), (44.443425, -92.233773), (44.333886, -91.927065), (44.202439, -91.877772), (44.032654, -91.592971), (43.994316, -91.43414), (43.775238, -91.242447), (43.616407, -91.269832), (43.501391, -91.215062), (43.501391, -91.368417), (43.501391, -96.451017), (45.297827, -96.451017), (45.412843, -96.681049), (45.604536, -96.856311), (45.818137, -96.582464), (45.933153, -96.560556), (46.332969, -96.598895), (46.437031, -96.719387), (46.656109, -96.801542), (46.924479, -96.785111), (46.968294, -96.823449), (47.609096, -96.856311), (47.948667, -97.053481), (48.140359, -97.130158), (48.545653, -97.16302), (48.682577, -97.097296), (49.000239, -97.228743), (49.000239, -95.152983), (49.383625, -95.152983), (49.372671, -94.955813), (49.295994, -94.824366), (48.775685, -94.69292), (48.715438, -94.588858), (48.699007, -94.260241), (48.649715, -94.221903), (48.627807, -93.838517), (48.518268, -93.794701), (48.545653, -93.466085), (48.589469, -93.466085), (48.644238, -93.208669), (48.62233, -92.984114), (48.540176, -92.726698), (48.436114, -92.655498), (48.447068, -92.50762), (48.222514, -92.370697), (48.315622, -92.304974), (48.359437, -92.053034), (48.266329, -92.009219), (48.200606, -91.713464), (48.112975, -91.713464), (48.041775, -91.565587), (48.080113, -91.264355), (48.178698, -91.083616), (48.238944, -90.837154), (48.091067, -90.749522), (48.123929, -90.579737), (48.091067, -90.377091), (48.112975, -90.141582), (47.987005, -89.873212), (48.008913, -89.615796), (47.954144, -89.637704), (47.828174, -89.971797), (47.729589, -90.437337), (47.625527, -90.738569), (47.368111, -91.171247), (47.20928, -91.357463), (47.028541, -91.642264), (46.787555, -92.091373), (46.705401, -92.014696)]], 'MI': [[(41.732339, -83.454238), (41.694001, -84.807042), (41.759724, -84.807042), (41.759724, -85.990061), (41.759724, -86.822556), (41.891171, -86.619909), (42.115725, -86.482986), (42.252649, -86.357016), (42.444341, -86.263908), (42.718189, -86.209139), (43.013943, -86.231047), (43.594499, -86.526801), (43.813577, -86.433693), (44.07647, -86.499417), (44.34484, -86.269385), (44.569394, -86.220093), (44.689887, -86.252954), (44.73918, -86.088646), (44.903488, -86.066738), (44.947303, -85.809322), (45.128042, -85.612152), (44.766564, -85.628583), (44.750133, -85.524521), (44.930872, -85.393075), (45.237581, -85.387598), (45.314258, -85.305444), (45.363551, -85.031597), (45.577151, -85.119228), (45.75789, -84.938489), (45.768844, -84.713934), (45.653829, -84.461995), (45.637398, -84.215533), (45.494997, -84.09504), (45.484043, -83.908824), (45.352597, -83.596638), (45.358074, -83.4871), (45.144473, -83.317314), (45.029457, -83.454238), (44.88158, -83.322791), (44.711795, -83.273499), (44.339363, -83.333745), (44.246255, -83.536392), (44.054562, -83.585684), (43.988839, -83.82667), (43.758807, -83.958116), (43.671176, -83.908824), (43.589022, -83.667839), (43.714992, -83.481623), (43.972408, -83.262545), (44.070993, -82.917498), (43.994316, -82.747713), (43.851915, -82.643651), (43.435668, -82.539589), (43.227544, -82.523158), (42.975605, -82.413619), (42.614127, -82.517681), (42.559357, -82.681989), (42.690804, -82.687466), (42.652465, -82.797005), (42.351234, -82.922975), (42.236218, -83.125621), (42.006186, -83.185868), (41.814493, -83.437807), (41.732339, -83.454238)], [(45.730506, -85.508091), (45.610013, -85.49166), (45.588105, -85.623106), (45.75789, -85.568337), (45.730506, -85.508091)], [(45.095181, -87.589328), (45.199243, -87.742682), (45.341643, -87.649574), (45.363551, -87.885083), (45.500474, -87.791975), (45.675736, -87.781021), (45.796229, -87.989145), (45.922199, -88.10416), (46.020784, -88.531362), (45.987922, -88.662808), (46.135799, -89.09001), (46.338446, -90.119674), (46.508231, -90.229213), (46.568478, -90.415429), (46.672539, -90.026566), (46.793032, -89.851304), (46.842325, -89.413149), (46.990202, -89.128348), (46.995679, -88.996902), (47.099741, -88.887363), (47.247618, -88.575177), (47.373588, -88.416346), (47.455742, -88.180837), (47.384542, -87.956283), (47.077833, -88.350623), (46.973771, -88.443731), (46.787555, -88.438254), (46.929956, -88.246561), (46.908048, -87.901513), (46.809463, -87.633143), (46.535616, -87.392158), (46.486323, -87.260711), (46.530139, -87.008772), (46.469893, -86.948526), (46.437031, -86.696587), (46.667063, -86.159846), (46.68897, -85.880522), (46.678016, -85.508091), (46.754694, -85.256151), (46.760171, -85.064458), (46.480847, -85.02612), (46.442508, -84.82895), (46.486323, -84.63178), (46.4206, -84.549626), (46.502754, -84.418179), (46.530139, -84.127902), (46.179615, -84.122425), (46.031737, -83.990978), (45.993399, -83.793808), (46.091984, -83.7719), (46.091984, -83.580208), (45.987922, -83.476146), (45.911245, -83.563777), (45.976968, -84.111471), (45.933153, -84.374364), (46.053645, -84.659165), (45.944106, -84.741319), (45.850998, -84.70298), (45.872906, -84.82895), (46.00983, -85.015166), (46.091984, -85.338305), (46.097461, -85.502614), (45.966014, -85.661445), (45.933153, -85.924338), (45.960537, -86.209139), (45.905768, -86.324155), (45.796229, -86.351539), (45.703121, -86.663725), (45.834568, -86.647294), (45.861952, -86.784218), (45.725029, -86.838987), (45.719552, -87.069019), (45.659305, -87.17308), (45.423797, -87.326435), (45.122565, -87.611236), (45.095181, -87.589328)], [(47.976051, -88.805209), (47.850082, -89.057148), (47.833651, -89.188594), (47.937713, -89.177641), (48.173221, -88.547792), (48.008913, -88.668285), (47.976051, -88.805209)]], 'RI': [[(41.67757, -71.196845), (41.496831, -71.120168), (41.474923, -71.317338), (41.67757, -71.196845)], [(42.01714, -71.530939), (42.01714, -71.383061), (41.781632, -71.328292), (41.710431, -71.22423), (41.726862, -71.344723), (41.578985, -71.448785), (41.370861, -71.481646), (41.321569, -71.859555), (41.414677, -71.799309), (42.006186, -71.799309), (42.01714, -71.530939)]], 'KS': [[(40.001626, -101.90605), (40.001626, -95.306337), (39.908518, -95.207752), (39.831841, -94.884612), (39.541563, -95.109167), (39.442978, -94.983197), (39.20747, -94.824366), (39.158177, -94.610765), (37.000263, -94.616242), (37.000263, -100.087706), (36.994786, -102.042974), (40.001626, -102.053927), (40.001626, -101.90605)]], 'MT': [[(49.000239, -104.047534), (47.861036, -104.042057), (45.944106, -104.047534), (44.996596, -104.042057), (44.996596, -104.058488), (45.002073, -105.91517), (45.002073, -109.080842), (45.002073, -111.05254), (44.476286, -111.047063), (44.580348, -111.227803), (44.75561, -111.386634), (44.547487, -111.616665), (44.509148, -111.819312), (44.563917, -111.868605), (44.520102, -112.104113), (44.569394, -112.241036), (44.481763, -112.471068), (44.48724, -112.783254), (44.394132, -112.887315), (44.448902, -113.002331), (44.772041, -113.133778), (44.782995, -113.341901), (44.865149, -113.456917), (45.056842, -113.45144), (45.128042, -113.571933), (45.330689, -113.736241), (45.522382, -113.834826), (45.604536, -113.807441), (45.703121, -113.98818), (45.593582, -114.086765), (45.456659, -114.333228), (45.560721, -114.546828), (45.670259, -114.497536), (45.774321, -114.568736), (45.88386, -114.387997), (46.037214, -114.492059), (46.272723, -114.464674), (46.645155, -114.322274), (46.639678, -114.612552), (46.705401, -114.623506), (46.809463, -114.886399), (46.919002, -114.930214), (47.187372, -115.302646), (47.258572, -115.324554), (47.302388, -115.527201), (47.42288, -115.718894), (47.696727, -115.724371), (47.976051, -116.04751), (49.000239, -116.04751), (48.994762, -111.50165), (49.000239, -109.453274), (49.000239, -104.047534)]], 'MS': [[(34.995703, -88.471115), (34.995703, -88.202745), (34.891641, -88.098683), (33.796253, -88.241084), (31.895754, -88.471115), (30.367688, -88.394438), (30.323872, -88.503977), (30.34578, -88.744962), (30.411504, -88.843547), (30.367688, -89.084533), (30.252672, -89.418626), (30.181472, -89.522688), (30.285534, -89.643181), (30.449842, -89.681519), (30.66892, -89.845827), (30.997536, -89.747242), (30.997536, -91.636787), (31.068736, -91.565587), (31.265906, -91.636787), (31.27686, -91.516294), (31.643815, -91.499863), (31.621907, -91.401278), (31.846462, -91.341032), (31.988862, -91.105524), (32.218894, -90.985031), (32.514649, -91.006939), (32.640618, -91.154816), (32.843265, -91.143862), (32.887081, -91.072662), (33.002096, -91.16577), (33.13902, -91.089093), (33.347144, -91.143862), (33.429298, -91.056231), (33.560744, -91.231493), (33.867453, -91.072662), (34.026284, -90.891923), (34.135823, -90.952169), (34.300131, -90.744046), (34.365854, -90.749522), (34.420624, -90.568783), (34.617794, -90.585214), (34.661609, -90.481152), (34.831394, -90.409952), (34.908072, -90.251121), (34.995703, -90.311367), (34.995703, -88.471115)]], 'PR': [[(17.984326, -66.448338), (18.006234, -66.771478), (17.929556, -66.924832), (17.973372, -66.985078), (17.956941, -67.209633), (18.19245, -67.154863), (18.362235, -67.269879), (18.515589, -67.094617), (18.488204, -66.957694), (18.488204, -66.409999), (18.433435, -65.840398), (18.367712, -65.632274), (18.203403, -65.626797), (18.186973, -65.730859), (18.017187, -65.834921), (17.929556, -66.234737), (17.984326, -66.448338)]], 'SC': [[(35.066903, -82.764143), (35.160011, -82.550543), (35.198349, -82.276696), (35.149057, -81.044384), (35.044995, -81.038907), (35.105241, -80.934845), (34.935456, -80.781491), (34.820441, -80.797922), (34.80401, -79.675149), (33.851022, -78.541422), (33.80173, -78.716684), (33.637421, -78.935762), (33.380005, -79.149363), (33.171881, -79.187701), (33.007573, -79.357487), (33.007573, -79.582041), (32.887081, -79.631334), (32.755634, -79.866842), (32.613234, -79.998289), (32.552987, -80.206412), (32.399633, -80.430967), (32.328433, -80.452875), (32.246279, -80.660998), (32.032678, -80.885553), (32.120309, -81.115584), (32.290094, -81.121061), (32.558464, -81.279893), (32.629664, -81.416816), (32.843265, -81.42777), (33.007573, -81.493493), (33.160928, -81.761863), (33.347144, -81.937125), (33.462159, -81.926172), (33.631944, -82.194542), (33.81816, -82.325988), (33.94413, -82.55602), (34.152254, -82.714851), (34.26727, -82.747713), (34.486347, -82.901067), (34.469916, -83.005129), (34.683517, -83.339222), (34.787579, -83.322791), (35.00118, -83.109191), (35.066903, -82.764143)]], 'KY': [[(38.769315, -83.903347), (38.632391, -83.678792), (38.703591, -83.519961), (38.626914, -83.142052), (38.725499, -83.032514), (38.758361, -82.890113), (38.588575, -82.846298), (38.561191, -82.731282), (38.424267, -82.594358), (38.123036, -82.621743), (37.931343, -82.50125), (37.783465, -82.342419), (37.668449, -82.293127), (37.553434, -82.101434), (37.537003, -81.969987), (37.268633, -82.353373), (37.120755, -82.720328), (37.044078, -82.720328), (36.978355, -82.868205), (36.890724, -82.879159), (36.852385, -83.070852), (36.742847, -83.136575), (36.600446, -83.673316), (36.584015, -83.689746), (36.594969, -84.544149), (36.627831, -85.289013), (36.616877, -85.486183), (36.655216, -86.592525), (36.633308, -87.852221), (36.677123, -88.071299), (36.496384, -88.054868), (36.507338, -89.298133), (36.496384, -89.418626), (36.622354, -89.363857), (36.578538, -89.215979), (36.983832, -89.133825), (37.038601, -89.183118), (37.213863, -89.029763), (37.224817, -88.914747), (37.071463, -88.547792), (37.153617, -88.421823), (37.285064, -88.514931), (37.389126, -88.476592), (37.482234, -88.065822), (37.657496, -88.15893), (37.799896, -88.027483), (37.893004, -87.934375), (37.903958, -87.682436), (37.975158, -87.600282), (37.903958, -87.512651), (37.93682, -87.381204), (37.788942, -87.129265), (37.893004, -87.047111), (37.991589, -86.795172), (37.893004, -86.729448), (37.931343, -86.504894), (38.040881, -86.521325), (38.166851, -86.302247), (38.051835, -86.263908), (37.958727, -86.039354), (38.024451, -85.924338), (38.27639, -85.83123), (38.325682, -85.655968), (38.451652, -85.590245), (38.533806, -85.42046), (38.730976, -85.431413), (38.68716, -85.173997), (38.780268, -84.987781), (38.785745, -84.812519), (39.059592, -84.894673), (39.103408, -84.817996), (39.103408, -84.43461), (38.895284, -84.231963), (38.807653, -84.215533), (38.769315, -83.903347)]], 'OR': [[(46.174138, -123.211348), (46.185092, -123.11824), (46.08103, -122.904639), (45.960537, -122.811531), (45.659305, -122.762239), (45.549767, -122.247407), (45.708598, -121.809251), (45.725029, -121.535404), (45.670259, -121.217742), (45.604536, -121.18488), (45.746937, -120.637186), (45.697644, -120.505739), (45.725029, -120.209985), (45.823614, -119.963522), (45.911245, -119.525367), (45.933153, -119.125551), (45.998876, -118.988627), (45.993399, -116.918344), (45.823614, -116.78142), (45.752413, -116.545912), (45.61549, -116.463758), (45.319735, -116.671881), (45.144473, -116.732128), (45.02398, -116.847143), (44.930872, -116.830713), (44.782995, -116.934774), (44.750133, -117.038836), (44.394132, -117.241483), (44.257209, -117.170283), (44.240778, -116.97859), (44.158624, -116.896436), (43.830007, -117.027882), (42.000709, -117.027882), (41.989755, -118.698349), (41.995232, -120.001861), (41.995232, -121.037003), (42.011663, -122.378853), (42.006186, -123.233256), (42.000709, -124.213628), (42.115725, -124.356029), (42.438865, -124.432706), (42.663419, -124.416275), (42.838681, -124.553198), (43.002989, -124.454613), (43.271359, -124.383413), (43.55616, -124.235536), (43.8081, -124.169813), (44.657025, -124.060274), (44.772041, -124.076705), (45.144473, -123.97812), (45.659305, -123.939781), (45.944106, -123.994551), (46.113892, -123.945258), (46.261769, -123.545441), (46.146753, -123.370179), (46.174138, -123.211348)]], 'SD': [[(45.944106, -104.047534), (45.933153, -96.560556), (45.818137, -96.582464), (45.604536, -96.856311), (45.412843, -96.681049), (45.297827, -96.451017), (43.501391, -96.451017), (43.479483, -96.582464), (43.397329, -96.527695), (43.222067, -96.560556), (43.123482, -96.434587), (43.052282, -96.511264), (42.855112, -96.544125), (42.707235, -96.631756), (42.488157, -96.44554), (42.515542, -96.626279), (42.657942, -96.692003), (42.844158, -97.217789), (42.844158, -97.688806), (42.866066, -97.831206), (42.767481, -97.951699), (42.94822, -98.466531), (42.997512, -98.499393), (42.997512, -101.626726), (43.002989, -103.324578), (43.002989, -104.053011), (44.996596, -104.058488), (44.996596, -104.042057), (45.944106, -104.047534)]]}
Assignment Image
![Python Assignment Description Image [Solution]](/images_solved/941988/11/im.webp)
Assignment Image
![Python Assignment Description Image [Solution]](/images_solved/941988/12/im.webp)
Assignment Image
![Python Assignment Description Image [Solution]](/images_solved/941988/13/im.webp)
Assignment Image
![Python Assignment Description Image [Solution]](/images_solved/941988/14/im.webp)