from colour import Color
import svgwrite
from ..error import DrawingFitError
from ..interval import Interval
MIN_PIXEL_ACCURACY = 1
[docs]def dynamic_label_color(color):
"""
calculates the luminance of a color and determines if a black or white label will be more contrasting
"""
color = Color(color)
if color.get_luminance() < 0.5:
return '#FFFFFF'
return '#000000'
[docs]class LabelMapping:
def __init__(self, **kwargs):
self._mapping = dict()
self._reverse_mapping = dict()
for attr, val in kwargs.items():
self[attr] = val
def __setitem__(self, key, value):
if key in self._mapping:
raise KeyError('duplicate key: keys must be unique', key)
if value in self._reverse_mapping:
raise KeyError('duplicate value: values must be unique', value)
self._mapping[key] = value
self._reverse_mapping[value] = key
[docs] def items(self):
return self._mapping.items()
def __getitem__(self, key):
return self._mapping[key]
[docs] def __len__(self):
return len(self._mapping.keys())
[docs] def get_key(self, value):
return self._reverse_mapping[value]
[docs] def set_key(self, key, value):
if key in self._mapping:
current_value = self._mapping[key]
if value == current_value:
return
elif value in self._reverse_mapping:
raise KeyError('duplicate value: values must be unique', value)
del self._mapping[key]
del self._reverse_mapping[current_value]
elif value in self._reverse_mapping:
raise KeyError('duplicate value: values must be unique', value)
self[key] = value
[docs] def add(self, value, prefix=''):
if value in self._reverse_mapping:
return self._reverse_mapping[value]
i = 1
while True:
key = '{}{}'.format(prefix, i)
if key not in self._mapping:
self[key] = value
break
i += 1
return self._reverse_mapping[value]
[docs]def split_intervals_into_tracks(intervals):
tracks = [[]]
for itvl in sorted(intervals, key=lambda x: x[0]):
added = False
for track in tracks:
overlaps = False
for track_itvl in track:
if Interval.overlaps(itvl, track_itvl):
overlaps = True
break
if not overlaps:
added = True
track.append(itvl)
break
if not added:
tracks.append([itvl])
return tracks
[docs]def generate_interval_mapping(
input_intervals, target_width, ratio, min_width,
buffer_length=None, start=None, end=None, min_inter_width=None,
min_pixel_accuracy=MIN_PIXEL_ACCURACY):
min_inter_width = min_width if min_inter_width is None else min_inter_width
if all([x is not None for x in [start, end, buffer_length]]):
raise AttributeError('buffer_length is a mutually exclusive argument with start/end')
intervals = []
for i in Interval.min_nonoverlapping(*input_intervals):
if not intervals or abs(Interval.dist(intervals[-1], i)) > 1:
intervals.append(i)
else:
intervals[-1] = intervals[-1] | i
# break up the intervals by any intervals of length 1
for itvl_in in input_intervals:
if len(itvl_in) > 1:
continue
# try splitting all current interval
temp = []
for itvl in intervals:
split = itvl - itvl_in
if split is not None:
temp.extend(split)
intervals = temp
for itvl_in in input_intervals:
if len(itvl_in) == 1:
intervals.append(itvl_in)
# now split any intervals by start/end
breaks = {}
for i in intervals:
# split by input intervals
breaks[i] = set([i.start, i.end])
for ii in input_intervals:
if ii.start >= i.start and ii.start <= i.end:
breaks[i].add(ii.start)
if ii.end >= i.start and ii.end <= i.end:
breaks[i].add(ii.end)
temp = []
for itvl, breakpoints in breaks.items():
breakpoints.add(itvl.start)
breakpoints.add(itvl.end)
pos = sorted(breakpoints)
if len(pos) == 1:
temp.append(Interval(pos[0]))
else:
# remove all the single intervals to start?
pos[0] -= 1
for i in range(1, len(pos)):
temp.append(Interval(pos[i - 1] + 1, pos[i]))
intervals = sorted(temp, key=lambda x: x.start)
if buffer_length is None:
buffer_length = 0
if start is None:
start = max(intervals[0].start - buffer_length, 1)
elif start <= 0:
raise AttributeError('start must be a natural number', start)
if end is None:
end = intervals[-1].end + buffer_length
elif end <= 0:
raise AttributeError('end must be a natural number', end)
total_length = end - start + 1
genic_length = sum([len(i) for i in intervals])
intergenic_length = total_length - genic_length
intermediate_intervals = 0
if start < intervals[0].start:
intermediate_intervals += 1
if end > intervals[-1].end:
intermediate_intervals += 1
for i in range(1, len(intervals)):
if intervals[i].start > intervals[i - 1].end + 1:
intermediate_intervals += 1
width = target_width - intermediate_intervals * min_inter_width - len(intervals) * min_width # reserved width
if width < 0:
raise DrawingFitError('width cannot accommodate the number of expected objects')
intergenic_width = width // (ratio + 1) if intergenic_length > 0 else 0
genic_width = width - intergenic_width
def intergenic_unit(x):
return x * intergenic_width / intergenic_length
def genic_unit(x):
return x * genic_width / genic_length
mapping = []
pos = 0
# do the intergenic region prior to the first genic region
if start < intervals[0].start:
ifrom = Interval(start, intervals[0].start - 1)
s = max(intergenic_unit(len(ifrom)), 0)
ito = Interval(pos, pos + min_inter_width + s)
mapping.append((ifrom, ito))
pos = ito.end
for i, curr in enumerate(intervals):
if i > 0 and intervals[i - 1].end + 1 < curr.start: # add between the intervals
prev = intervals[i - 1]
ifrom = Interval(prev.end + 1, curr.start - 1)
s = max(intergenic_unit(len(ifrom)), 0)
ito = Interval(pos, pos + min_inter_width + s)
mapping.append((ifrom, ito))
pos = ito.end
s = max(genic_unit(len(curr)), 0)
assert s >= 0
ito = Interval(pos, pos + min_width + s)
mapping.append((curr, ito))
pos = ito.end
# now the last intergenic region will make up for the rounding error
if end > intervals[-1].end:
ifrom = Interval(intervals[-1].end + 1, end)
s = max(intergenic_unit(len(ifrom)), 0)
ito = Interval(pos, pos + min_inter_width + s)
mapping.append((ifrom, ito))
pos = ito.end
# mapping[-1][1].end = target_width # min(int(target_width), mapping[-1][1].end)
if abs(mapping[-1][1].end - target_width) > min_pixel_accuracy:
raise AssertionError(
'end is off by more than the expected pixel allowable error',
mapping[-1][1].end, target_width, min_pixel_accuracy)
temp = mapping
mapping = dict()
for ifrom, ito in temp:
mapping[ifrom] = ito
# assert that that mapping is correct
for ifrom in input_intervals:
ifrom = Interval(ifrom.start, ifrom.end)
p1 = Interval.convert_ratioed_pos(mapping, ifrom.start)
p2 = Interval.convert_ratioed_pos(mapping, ifrom.end)
if ifrom in mapping and ito.end == target_width:
continue
n = p1 | p2
if n.length() < min_width and abs(n.length() - min_width) > min_pixel_accuracy: # precision error allowable
raise AssertionError(
'interval mapping should not map any intervals to less than the minimum required width. Interval {}'
' was mapped to a pixel interval of length {} but the minimum width is {}'.format(
ifrom, n.length(), min_width),
p1, p2, mapping,
input_intervals, target_width, ratio, min_width, buffer_length, start, end, min_inter_width)
return mapping
[docs]class Tag(svgwrite.base.BaseElement):
def __init__(self, elementname, content='', **kwargs):
self.elementname = elementname
super(Tag, self).__init__(**kwargs)
self.content = content
[docs] def get_xml(self):
xml = super(Tag, self).get_xml()
xml.text = self.content
return xml