Beispiel #1
0
 def _draw_circle_segment(
     self,
     dr: svgwrite.Drawing,
     tracks: List[Track],
     a1: float,
     a2: float,
     rr: ValueRange,
     center: XY,
 ):
     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)
     r1 = rr.lower()
     r2 = (
         rr.lower()
         + rr.diameter() * length / self.poster.length_range_by_date.upper()
     )
     sin_a1, cos_a1 = math.sin(a1), math.cos(a1)
     sin_a2, cos_a2 = math.sin(a2), math.cos(a2)
     path = dr.path(
         d=("M", center.x + r1 * sin_a1, center.y - r1 * cos_a1),
         fill=color,
         stroke="none",
     )
     path.push("l", (r2 - r1) * sin_a1, (r1 - r2) * cos_a1)
     path.push(f"a{r2},{r2} 0 0,0 {r2 * (sin_a2 - sin_a1)},{r2 * (cos_a1 - cos_a2)}")
     path.push("l", (r1 - r2) * sin_a2, (r2 - r1) * cos_a2)
     dr.add(path)
Beispiel #2
0
 def _draw_circle_segment(
     self,
     dr: svgwrite.Drawing,
     g: svgwrite.container.Group,
     tracks: typing.List[Track],
     a1: float,
     a2: float,
     rr: ValueRange,
     center: XY,
 ) -> None:
     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)
     max_length = self.poster.length_range_by_date.upper()
     assert max_length is not None
     r1 = rr.lower()
     assert r1 is not None
     r2 = rr.interpolate((length / max_length).magnitude)
     sin_a1, cos_a1 = math.sin(a1), math.cos(a1)
     sin_a2, cos_a2 = math.sin(a2), math.cos(a2)
     path = dr.path(
         d=("M", center.x + r1 * sin_a1, center.y - r1 * cos_a1),
         fill=color,
         stroke="none",
     )
     path.push("l", (r2 - r1) * sin_a1, (r1 - r2) * cos_a1)
     path.push(
         f"a{r2},{r2} 0 0,0 {r2 * (sin_a2 - sin_a1)},{r2 * (cos_a1 - cos_a2)}"
     )
     path.push("l", (r1 - r2) * sin_a2, (r2 - r1) * cos_a2)
     date_title = str(tracks[0].start_time().date())
     str_length = utils.format_float(self.poster.m2u(length))
     path.set_desc(title=f"{date_title} {str_length} {self.poster.u()}")
     g.add(path)
Beispiel #3
0
def compute_bounds_xy(lines: typing.List[typing.List[XY]]) -> typing.Tuple[ValueRange, ValueRange]:
    range_x = ValueRange()
    range_y = ValueRange()
    for line in lines:
        for xy in line:
            range_x.extend(xy.x)
            range_y.extend(xy.y)
    return range_x, range_y
Beispiel #4
0
 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
Beispiel #5
0
    def color(self,
              length_range: ValueRange,
              length: float,
              is_special: bool = False) -> str:
        assert length_range.is_valid()
        assert length_range.contains(length)

        color1 = (self.poster.colors["special"]
                  if is_special else self.poster.colors["track"])
        color2 = (self.poster.colors["special2"]
                  if is_special else self.poster.colors["track2"])

        diff = length_range.diameter()
        if diff == 0:
            return color1

        return utils.interpolate_color(color1, color2,
                                       (length - length_range.lower()) / diff)
Beispiel #6
0
 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
Beispiel #7
0
    def set_tracks(self, tracks):
        """Associate the set of tracks with this poster.

        In addition to setting self.tracks, also compute the necessary attributes for the Poster
        based on this set of tracks.
        """
        self.tracks = tracks
        self.tracks_by_date = {}
        self.length_range = ValueRange()
        self.length_range_by_date = ValueRange()
        self.__compute_years(tracks)
        for track in tracks:
            if not self.years.contains(track.start_time):
                continue
            text_date = track.start_time.strftime("%Y-%m-%d")
            if text_date in self.tracks_by_date:
                self.tracks_by_date[text_date].append(track)
            else:
                self.tracks_by_date[text_date] = [track]
            self.length_range.extend(track.length)
        for tracks in self.tracks_by_date.values():
            length = sum([t.length for t in tracks])
            self.length_range_by_date.extend(length)
Beispiel #8
0
 def __compute_track_statistics(self):
     length_range = ValueRange()
     total_length = 0
     weeks = {}
     for t in self.tracks:
         total_length += t.length
         length_range.extend(t.length)
         # time.isocalendar()[1] -> week number
         weeks[(t.start_time.year, t.start_time.isocalendar()[1])] = 1
     return (
         total_length,
         total_length / len(self.tracks),
         length_range.lower(),
         length_range.upper(),
         len(weeks),
     )
Beispiel #9
0
    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)
Beispiel #10
0
class Poster:
    """Create a poster from track data.

    Attributes:
        athlete: Name of athlete to be displayed on poster.
        title: Title of poster.
        tracks_by_date: Tracks organized temporally if needed.
        tracks: List of tracks to be used in the poster.
        length_range: Range of lengths of tracks in poster.
        length_range_by_date: Range of lengths organized temporally.
        units: Length units to be used in poster.
        colors: Colors for various components of the poster.
        width: Poster width.
        height: Poster height.
        years: Years included in the poster.
        tracks_drawer: drawer used to draw the poster.

    Methods:
        set_tracks: Associate the Poster with a set of tracks
        draw: Draw the tracks on the poster.
        m2u: Convert meters to kilometers or miles based on units
        u: Return distance unit (km or mi)
    """
    def __init__(self):
        self.athlete = None
        self.title = None
        self.tracks_by_date = {}
        self.tracks = []
        self.length_range = None
        self.length_range_by_date = None
        self.units = "metric"
        self.colors = {
            "background": "#222222",
            "text": "#FFFFFF",
            "special": "#FFFF00",
            "track": "#4DD2FF",
        }
        self.special_distance = {
            "special_distance1": "10",
            "special_distance2": "20"
        }
        self.width = 200
        self.height = 300
        self.years = None
        self.tracks_drawer = None
        self.trans = None
        self.set_language(None)

    def set_language(self, language):
        if language:
            try:
                locale.setlocale(locale.LC_ALL, f"{language}.utf8")
            except locale.Error as e:
                print(f'Cannot set locale to "{language}": {e}')
                language = None
                pass

        # Fall-back to NullTranslations, if the specified language translation cannot be found.
        if language:
            lang = gettext.translation("gpxposter",
                                       localedir="locale",
                                       languages=[language],
                                       fallback=True)
        else:
            lang = gettext.NullTranslations()
        self.trans = lang.gettext

    def set_tracks(self, tracks):
        """Associate the set of tracks with this poster.

        In addition to setting self.tracks, also compute the necessary attributes for the Poster
        based on this set of tracks.
        """
        self.tracks = tracks
        self.tracks_by_date = {}
        self.length_range = ValueRange()
        self.length_range_by_date = ValueRange()
        self.__compute_years(tracks)
        for track in tracks:
            if not self.years.contains(track.start_time):
                continue
            text_date = track.start_time.strftime("%Y-%m-%d")
            if text_date in self.tracks_by_date:
                self.tracks_by_date[text_date].append(track)
            else:
                self.tracks_by_date[text_date] = [track]
            self.length_range.extend(track.length)
        for tracks in self.tracks_by_date.values():
            length = sum([t.length for t in tracks])
            self.length_range_by_date.extend(length)

    def draw(self, drawer, output):
        """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 m2u(self, m):
        """Convert meters to kilometers or miles, according to units."""
        if self.units == "metric":
            return 0.001 * m
        return 0.001 * m / 1.609344

    def u(self):
        """Return the unit of distance being used on the Poster."""
        if self.units == "metric":
            return "km"
        return "mi"

    def format_distance(self, d: float) -> str:
        """Formats a distance using the locale specific float format and the selected unit."""
        return format_float(self.m2u(d)) + " " + self.u()

    def __draw_tracks(self, d, size: XY, offset: XY):
        self.tracks_drawer.draw(d, size, offset)

    def __draw_header(self, d):
        text_color = self.colors["text"]
        title_style = "font-size:12px; font-family:Arial; font-weight:bold;"
        d.add(
            d.text(self.title,
                   insert=(10, 20),
                   fill=text_color,
                   style=title_style))

    def __draw_footer(self, d):
        text_color = self.colors["text"]
        header_style = "font-size:4px; font-family:Arial"
        value_style = "font-size:9px; font-family:Arial"
        small_value_style = "font-size:3px; font-family:Arial"

        (
            total_length,
            average_length,
            min_length,
            max_length,
            weeks,
        ) = self.__compute_track_statistics()

        d.add(
            d.text(
                self.trans("ATHLETE"),
                insert=(10, self.height - 20),
                fill=text_color,
                style=header_style,
            ))
        d.add(
            d.text(
                self.athlete,
                insert=(10, self.height - 10),
                fill=text_color,
                style=value_style,
            ))
        d.add(
            d.text(
                self.trans("STATISTICS"),
                insert=(120, self.height - 20),
                fill=text_color,
                style=header_style,
            ))
        d.add(
            d.text(
                self.trans("Number") + f": {len(self.tracks)}",
                insert=(120, self.height - 15),
                fill=text_color,
                style=small_value_style,
            ))
        d.add(
            d.text(
                self.trans("Weekly") + ": " +
                format_float(len(self.tracks) / weeks),
                insert=(120, self.height - 10),
                fill=text_color,
                style=small_value_style,
            ))
        d.add(
            d.text(
                self.trans("Total") + ": " +
                self.format_distance(total_length),
                insert=(139, self.height - 15),
                fill=text_color,
                style=small_value_style,
            ))
        d.add(
            d.text(
                self.trans("Avg") + ": " +
                self.format_distance(average_length),
                insert=(139, self.height - 10),
                fill=text_color,
                style=small_value_style,
            ))
        d.add(
            d.text(
                self.trans("Min") + ": " + self.format_distance(min_length),
                insert=(167, self.height - 15),
                fill=text_color,
                style=small_value_style,
            ))
        d.add(
            d.text(
                self.trans("Max") + ": " + self.format_distance(max_length),
                insert=(167, self.height - 10),
                fill=text_color,
                style=small_value_style,
            ))

    def __compute_track_statistics(self):
        length_range = ValueRange()
        total_length = 0
        total_length_year_dict = defaultdict(int)
        weeks = {}
        for t in self.tracks:
            total_length += t.length
            total_length_year_dict[t.start_time.year] += t.length
            length_range.extend(t.length)
            # time.isocalendar()[1] -> week number
            weeks[(t.start_time.year, t.start_time.isocalendar()[1])] = 1
        self.total_length_year_dict = total_length_year_dict
        return (
            total_length,
            total_length / len(self.tracks),
            length_range.lower(),
            length_range.upper(),
            len(weeks),
        )

    def __compute_years(self, tracks):
        if self.years is not None:
            return
        self.years = YearRange()
        for t in tracks:
            self.years.add(t.start_time)