def project( bbox: s2sphere.LatLngRect, size: XY, offset: XY, latlnglines: typing.List[typing.List[s2sphere.LatLng]] ) -> typing.List[typing.List[typing.Tuple[float, float]]]: min_x = lng2x(bbox.lng_lo().degrees) d_x = lng2x(bbox.lng_hi().degrees) - min_x while d_x >= 2: d_x -= 2 while d_x < 0: d_x += 2 min_y = lat2y(bbox.lat_lo().degrees) max_y = lat2y(bbox.lat_hi().degrees) d_y = abs(max_y - min_y) scale = size.x / d_x if size.x / size.y <= d_x / d_y else size.y / d_y offset = offset + 0.5 * (size - scale * XY(d_x, -d_y)) - scale * XY(min_x, min_y) lines = [] for latlngline in latlnglines: line = [] for latlng in latlngline: if bbox.contains(latlng): line.append((offset + scale * latlng2xy(latlng)).tuple()) else: if len(line) > 0: lines.append(line) line = [] if len(line) > 0: lines.append(line) return lines
def draw(self, dr: svgwrite.Drawing, g: svgwrite.container.Group, size: XY, offset: XY) -> None: """For each track, draw it on the poster.""" if self.poster.tracks is None: raise PosterError("No tracks to draw.") cell_size, counts = utils.compute_grid(len(self.poster.tracks), size) if cell_size is None or counts is None: raise PosterError("Unable to compute grid.") count_x, count_y = counts[0], counts[1] spacing_x = 0 if count_x <= 1 else (size.x - cell_size * count_x) / ( count_x - 1) spacing_y = 0 if count_y <= 1 else (size.y - cell_size * count_y) / ( count_y - 1) offset.x += (size.x - count_x * cell_size - (count_x - 1) * spacing_x) / 2 offset.y += (size.y - count_y * cell_size - (count_y - 1) * spacing_y) / 2 year_groups: typing.Dict[int, svgwrite.container.Group] = {} for (index, tr) in enumerate(self.poster.tracks): year = tr.start_time().year if year not in year_groups: g_year = dr.g(id=f"year{year}") g.add(g_year) year_groups[year] = g_year else: g_year = year_groups[year] p = XY(index % count_x, index // count_x) * XY( cell_size + spacing_x, cell_size + spacing_y) self._draw_track( dr, g_year, tr, 0.9 * XY(cell_size, cell_size), offset + 0.05 * XY(cell_size, cell_size) + p, )
def draw(self, dr: svgwrite.Drawing, size: XY, offset: XY) -> None: """For each track, draw it on the poster.""" if self.poster.tracks is None: raise PosterError("No tracks to draw.") cell_size, counts = utils.compute_grid(len(self.poster.tracks), size) if cell_size is None or counts is None: raise PosterError("Unable to compute grid.") count_x, count_y = counts[0], counts[1] spacing_x = 0 if count_x <= 1 else (size.x - cell_size * count_x) / ( count_x - 1) spacing_y = 0 if count_y <= 1 else (size.y - cell_size * count_y) / ( count_y - 1) offset.x += (size.x - count_x * cell_size - (count_x - 1) * spacing_x) / 2 offset.y += (size.y - count_y * cell_size - (count_y - 1) * spacing_y) / 2 for (index, tr) in enumerate(self.poster.tracks): p = XY(index % count_x, index // count_x) * XY( cell_size + spacing_x, cell_size + spacing_y) self._draw_track( dr, tr, 0.9 * XY(cell_size, cell_size), offset + 0.05 * XY(cell_size, cell_size) + p, )
def draw(self, drawer: "TracksDrawer", output: str) -> None: """Set the Poster's drawer and draw the tracks.""" self.tracks_drawer = drawer d = svgwrite.Drawing(output, (f"{self.width}mm", f"{self.height}mm")) d.viewbox(0, 0, self.width, self.height) d.add(d.rect((0, 0), (self.width, self.height), fill=self.colors["background"])) self._draw_header(d) self._draw_footer(d) self._draw_tracks(d, XY(self.width - 20, self.height - 30 - 30), XY(10, 30)) d.save()
def _draw_rings(self, dr: svgwrite.Drawing, g: svgwrite.container.Group, center: XY, radius_range: ValueRange) -> None: length_range = self.poster.length_range_by_date if not length_range.is_valid(): return min_length = length_range.lower() max_length = length_range.upper() assert min_length is not None assert max_length is not None ring_distance = self._determine_ring_distance(max_length) if ring_distance is None: return distance = ring_distance while distance < max_length: radius = radius_range.interpolate( (distance / max_length).magnitude) g.add( dr.circle( center=center.tuple(), r=radius, stroke=self._ring_color, stroke_opacity="0.2", fill="none", stroke_width=0.3, )) distance += ring_distance
def draw(self, dr: svgwrite.Drawing, g: svgwrite.container.Group, size: XY, offset: XY) -> None: """Draw the circular Poster using distances broken down by time""" if self.poster.tracks is None: raise PosterError("No tracks to draw.") if self.poster.length_range_by_date is None: return years = self.poster.years.count() _, counts = utils.compute_grid(years, size) if counts is None: raise PosterError("Unable to compute grid.") count_x, count_y = counts[0], counts[1] x, y = 0, 0 cell_size = size * XY(1 / count_x, 1 / count_y) margin = XY(4, 4) if count_x <= 1: margin.x = 0 if count_y <= 1: margin.y = 0 sub_size = cell_size - 2 * margin for year in self.poster.years.iter(): g_year = dr.g(id=f"year{year}") g.add(g_year) self._draw_year(dr, g_year, sub_size, offset + margin + cell_size * XY(x, y), year) x += 1 if x >= count_x: x = 0 y += 1
def draw(self, dr: svgwrite.Drawing, g: svgwrite.container.Group, size: XY, offset: XY) -> None: """Iterate through the Poster's years, creating a calendar for each.""" if self.poster.tracks is None: raise PosterError("No tracks to draw.") years = self.poster.years.count() _, counts = utils.compute_grid(years, size) if counts is None: raise PosterError("Unable to compute grid.") count_x, count_y = counts[0], counts[1] x, y = 0, 0 cell_size = size * XY(1 / count_x, 1 / count_y) margin = XY(4, 8) if count_x <= 1: margin.x = 0 if count_y <= 1: margin.y = 0 sub_size = cell_size - 2 * margin for year in self.poster.years.iter(): g_year = dr.g(id=f"year{year}") g.add(g_year) self._draw(dr, g_year, sub_size, offset + margin + cell_size * XY(x, y), year) x += 1 if x >= count_x: x = 0 y += 1
def draw(self, dr: svgwrite.Drawing, size: XY, offset: XY): """Iterate through the Poster's years, creating a calendar for each.""" if self.poster.tracks is None: raise PosterError("No tracks to draw.") years = self.poster.years.count() _, counts = utils.compute_grid(years, size) if counts is None: raise PosterError("Unable to compute grid.") count_x, count_y = counts[0], counts[1] x, y = 0, 0 cell_size = size * XY(1 / count_x, 1 / count_y) margin = XY(4, 8) if count_x <= 1: margin.x = 0 if count_y <= 1: margin.y = 0 sub_size = cell_size - 2 * margin for year in range(self.poster.years.from_year, self.poster.years.to_year + 1): self._draw(dr, sub_size, offset + margin + cell_size * XY(x, y), year) x += 1 if x >= count_x: x = 0 y += 1
def _draw_rings(self, dr: svgwrite.Drawing, center: XY, radius_range: ValueRange): length_range = self.poster.length_range_by_date ring_distance = self._determine_ring_distance() if ring_distance is None: return distance = ring_distance while distance < length_range.upper(): radius = ( radius_range.lower() + radius_range.diameter() * distance / length_range.upper() ) dr.add( dr.circle( center=center.tuple(), r=radius, stroke=self._ring_color, stroke_opacity="0.2", fill="none", stroke_width=0.3, ) ) distance += ring_distance
def _draw_year(self, dr: svgwrite.Drawing, g: svgwrite.container.Group, size: XY, offset: XY, year: int) -> None: min_size = min(size.x, size.y) outer_radius = 0.5 * min_size - 6 radius_range = ValueRange.from_pair(outer_radius / 4, outer_radius) center = offset + 0.5 * size if self._rings: self._draw_rings(dr, g, center, radius_range) year_style = f"dominant-baseline: central; font-size:{min_size * 4.0 / 80.0}px; font-family:Arial;" month_style = f"font-size:{min_size * 3.0 / 80.0}px; font-family:Arial;" g.add( dr.text( f"{year}", insert=center.tuple(), fill=self.poster.colors["text"], text_anchor="middle", alignment_baseline="middle", style=year_style, )) df = 360.0 / (366 if calendar.isleap(year) else 365) day = 0 date = datetime.date(year, 1, 1) while date.year == year: text_date = date.strftime("%Y-%m-%d") a1 = math.radians(day * df) a2 = math.radians((day + 1) * df) if date.day == 1: (_, last_day) = calendar.monthrange(date.year, date.month) a3 = math.radians((day + last_day - 1) * df) sin_a1, cos_a1 = math.sin(a1), math.cos(a1) sin_a3, cos_a3 = math.sin(a3), math.cos(a3) r1 = outer_radius + 1 r2 = outer_radius + 6 r3 = outer_radius + 2 g.add( dr.line( start=(center + r1 * XY(sin_a1, -cos_a1)).tuple(), end=(center + r2 * XY(sin_a1, -cos_a1)).tuple(), stroke=self.poster.colors["text"], stroke_width=0.3, )) path = dr.path( d=("M", center.x + r3 * sin_a1, center.y - r3 * cos_a1), fill="none", stroke="none", ) path.push( f"a{r3},{r3} 0 0,1 {r3 * (sin_a3 - sin_a1)},{r3 * (cos_a1 - cos_a3)}" ) g.add(path) tpath = svgwrite.text.TextPath( path, self.poster.month_name(date.month), startOffset=(0.5 * r3 * (a3 - a1))) text = dr.text( "", fill=self.poster.colors["text"], text_anchor="middle", style=month_style, ) text.add(tpath) g.add(text) if text_date in self.poster.tracks_by_date: self._draw_circle_segment( dr, g, self.poster.tracks_by_date[text_date], a1, a2, radius_range, center, ) day += 1 date += datetime.timedelta(1)
def _draw(self, dr: svgwrite.Drawing, g: svgwrite.container.Group, size: XY, offset: XY, year: int) -> None: min_size = min(size.x, size.y) year_size = min_size * 4.0 / 80.0 year_style = f"font-size:{year_size}px; font-family:Arial;" month_style = f"font-size:{min_size * 3.0 / 80.0}px; font-family:Arial;" day_style = f"dominant-baseline: central; font-size:{min_size * 1.0 / 80.0}px; font-family:Arial;" day_length_style = f"font-size:{min_size * 1.0 / 80.0}px; font-family:Arial;" g.add( dr.text( f"{year}", insert=offset.tuple(), fill=self.poster.colors["text"], alignment_baseline="hanging", style=year_style, )) offset.y += year_size size.y -= year_size count_x = 31 for month in range(1, 13): date = datetime.date(year, month, 1) (_, last_day) = calendar.monthrange(year, month) count_x = max(count_x, date.weekday() + last_day) cell_size = min(size.x / count_x, size.y / 36) spacing = XY( (size.x - cell_size * count_x) / (count_x - 1), (size.y - cell_size * 3 * 12) / 11, ) for month in range(1, 13): date = datetime.date(year, month, 1) y = month - 1 y_pos = offset.y + (y * 3 + 1) * cell_size + y * spacing.y g.add( dr.text( self.poster.month_name(month), insert=(offset.x, y_pos - 2), fill=self.poster.colors["text"], alignment_baseline="hanging", style=month_style, )) day_offset = date.weekday() while date.month == month: x = date.day - 1 x_pos = offset.x + (day_offset + x) * cell_size + x * spacing.x pos = (x_pos + 0.05 * cell_size, y_pos + 1.15 * cell_size) dim = (cell_size * 0.9, cell_size * 0.9) text_date = date.strftime("%Y-%m-%d") if text_date in self.poster.tracks_by_date: tracks = self.poster.tracks_by_date[text_date] length = sum([t.length() for t in tracks]) has_special = len([t for t in tracks if t.special]) > 0 color = self.color(self.poster.length_range_by_date, length, has_special) g.add(dr.rect(pos, dim, fill=color)) g.add( dr.text( utils.format_float(self.poster.m2u(length)), insert=( pos[0] + cell_size / 2, pos[1] + cell_size + cell_size / 2, ), text_anchor="middle", style=day_length_style, fill=self.poster.colors["text"], )) else: g.add(dr.rect(pos, dim, fill="#444444")) g.add( dr.text( localized_day_of_week_name(date.weekday(), short=True), insert=( offset.x + (day_offset + x) * cell_size + cell_size / 2, pos[1] + cell_size / 2, ), text_anchor="middle", alignment_baseline="middle", style=day_style, )) date += datetime.timedelta(1)
def draw(self, dr: svgwrite.Drawing, g: svgwrite.container.Group, size: XY, offset: XY) -> None: if self.poster.tracks is None: raise PosterError("No tracks to draw") year_size = 200 * 4.0 / 80.0 year_style = f"font-size:{year_size}px; font-family:Arial;" year_length_style = f"font-size:{110 * 3.0 / 80.0}px; font-family:Arial;" month_names_style = "font-size:2.5px; font-family:Arial" total_length_year_dict = self.poster.total_length_year_dict for year in self.poster.years.iter(): g_year = dr.g(id=f"year{year}") g.add(g_year) start_date_weekday, _ = calendar.monthrange(year, 1) github_rect_first_day = datetime.date(year, 1, 1) # Github profile the first day start from the last Monday of the last year or the first Monday of this year # It depands on if the first day of this year is Monday or not. github_rect_day = github_rect_first_day + datetime.timedelta( -start_date_weekday) year_length = total_length_year_dict.get(year, 0) year_length_str = utils.format_float(self.poster.m2u(year_length)) month_names = [ locale.nl_langinfo(day)[:3] # Get only first three letters for day in [ locale.MON_1, locale.MON_2, locale.MON_3, locale.MON_4, locale.MON_5, locale.MON_6, locale.MON_7, locale.MON_8, locale.MON_9, locale.MON_10, locale.MON_11, locale.MON_12, ] ] km_or_mi = self.poster.u() g_year.add( dr.text( f"{year}", insert=offset.tuple(), fill=self.poster.colors["text"], alignment_baseline="hanging", style=year_style, )) g_year.add( dr.text( f"{year_length_str} {km_or_mi}", insert=(offset.tuple()[0] + 165, offset.tuple()[1] + 2), fill=self.poster.colors["text"], alignment_baseline="hanging", style=year_length_style, )) # add month name up to the poster one by one because of svg text auto trim the spaces. for num, name in enumerate(month_names): g_year.add( dr.text( f"{name}", insert=(offset.tuple()[0] + 15.5 * num, offset.tuple()[1] + 14), fill=self.poster.colors["text"], style=month_names_style, )) rect_x = 10.0 dom = (2.6, 2.6) # add every day of this year for 53 weeks and per week has 7 days for _i in range(54): rect_y = offset.y + year_size + 2 for _j in range(7): if int(github_rect_day.year) > year: break rect_y += 3.5 color = "#444444" date_title = str(github_rect_day) if date_title in self.poster.tracks_by_date: tracks = self.poster.tracks_by_date[date_title] length = sum([t.length() for t in tracks]) distance1 = self.poster.special_distance[ "special_distance"] distance2 = self.poster.special_distance[ "special_distance2"] has_special = distance1 < length < distance2 color = self.color(self.poster.length_range_by_date, length, has_special) if length >= distance2: special_color = self.poster.colors.get( "special2") or self.poster.colors.get( "special") if special_color is not None: color = special_color str_length = utils.format_float( self.poster.m2u(length)) date_title = f"{date_title} {str_length} {km_or_mi}" rect = dr.rect((rect_x, rect_y), dom, fill=color) rect.set_desc(title=date_title) g_year.add(rect) github_rect_day += datetime.timedelta(1) rect_x += 3.5 offset.y += 3.5 * 9 + year_size + 1.5
def latlng2xy(latlng: s2sphere.LatLng) -> XY: return XY(lng2x(latlng.lng().degrees), lat2y(latlng.lat().degrees))
def _draw(self, dr: svgwrite.Drawing, size: XY, offset: XY, year: int): min_size = min(size.x, size.y) year_size = min_size * 4.0 / 80.0 year_style = f"font-size:{year_size}px; font-family:Arial;" month_style = f"font-size:{min_size * 3.0 / 80.0}px; font-family:Arial;" day_style = f"dominant-baseline: central; font-size:{min_size * 1.0 / 80.0}px; font-family:Arial;" day_length_style = f"font-size:{min_size * 1.0 / 80.0}px; font-family:Arial;" dr.add( dr.text( f"{year}", insert=offset.tuple(), fill=self.poster.colors["text"], alignment_baseline="hanging", style=year_style, ) ) offset.y += year_size size.y -= year_size count_x = 31 for month in range(1, 13): date = datetime.date(year, month, 1) (_, last_day) = calendar.monthrange(year, month) count_x = max(count_x, date.weekday() + last_day) cell_size = min(size.x / count_x, size.y / 36) spacing = XY( (size.x - cell_size * count_x) / (count_x - 1), (size.y - cell_size * 3 * 12) / 11, ) # chinese weekday key number is the third. keyword_num = 0 if locale.getlocale()[0] == "zh_CN": keyword_num = 2 # first character of localized day names, starting with Monday. dow = [ locale.nl_langinfo(day)[keyword_num].upper() for day in [ locale.DAY_2, locale.DAY_3, locale.DAY_4, locale.DAY_5, locale.DAY_6, locale.DAY_7, locale.DAY_1, ] ] for month in range(1, 13): date = datetime.date(year, month, 1) y = month - 1 y_pos = offset.y + (y * 3 + 1) * cell_size + y * spacing.y dr.add( dr.text( date.strftime("%B"), insert=(offset.x, y_pos - 2), fill=self.poster.colors["text"], alignment_baseline="hanging", style=month_style, ) ) day_offset = date.weekday() while date.month == month: x = date.day - 1 x_pos = offset.x + (day_offset + x) * cell_size + x * spacing.x pos = (x_pos + 0.05 * cell_size, y_pos + 1.15 * cell_size) dim = (cell_size * 0.9, cell_size * 0.9) text_date = date.strftime("%Y-%m-%d") if text_date in self.poster.tracks_by_date: tracks = self.poster.tracks_by_date[text_date] length = sum([t.length for t in tracks]) has_special = len([t for t in tracks if t.special]) > 0 color = self.color( self.poster.length_range_by_date, length, has_special ) dr.add(dr.rect(pos, dim, fill=color)) dr.add( dr.text( utils.format_float(self.poster.m2u(length)), insert=( pos[0] + cell_size / 2, pos[1] + cell_size + cell_size / 2, ), text_anchor="middle", style=day_length_style, fill=self.poster.colors["text"], ) ) else: dr.add(dr.rect(pos, dim, fill="#444444")) dr.add( dr.text( dow[date.weekday()], insert=( offset.x + (day_offset + x) * cell_size + cell_size / 2, pos[1] + cell_size / 2, ), text_anchor="middle", alignment_baseline="middle", style=day_style, ) ) date += datetime.timedelta(1)