444 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			444 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Python
		
	
	
	
| """
 | |
| 
 | |
| Basisklasse für alle Darstellungsseiten
 | |
| 
 | |
| Hinweise zu Cairo:
 | |
| Das Koordinatensystem geht von (0, 0) links oben bis (400, 300) rechts unten.
 | |
| Um exakte Pixel zu treffen müssen Koordinaten mit Offset 0.5 verwendet werden.
 | |
| 
 | |
| """
 | |
| import os
 | |
| import cairo
 | |
| import math
 | |
| from datetime import datetime
 | |
| 
 | |
| class Page():
 | |
| 
 | |
|     pageno = 1                # Nummer der aktuell sichtbaren Seite
 | |
|     backlight = False
 | |
|     color_normal = "dcdcdc"   # Standardhintergrund
 | |
|     color_lighted = "d89090"  # Hintergrund im Nachtmodus
 | |
| 
 | |
|     bgcolor = (0.86, 0.86, 0.86)
 | |
|     fgcolor = (0, 0, 0)
 | |
| 
 | |
|     @staticmethod
 | |
|     def hexcolor(hexstr):
 | |
|         if (len(hexstr) != 6) or (not all(c.lower in '0123456789abcdef' for c in hexstr)):
 | |
|             raise ValueError('Not a valid RGB Hexstring')
 | |
|         else:
 | |
|             return(int(hexstr[0:2], 16) / 255.0,
 | |
|                    int(hexstr[2:4], 16) / 255.0,
 | |
|                    int(hexstr[4:6], 16) / 255.0)
 | |
| 
 | |
|     @staticmethod
 | |
|     def rotate (origin, points, angle):
 | |
|         # operates on tuples, angle in degrees
 | |
|         ox, oy = origin
 | |
|         phi = math.radians(angle)
 | |
|         fs = math.sin(phi)
 | |
|         fc = math.cos(phi)
 | |
|         rotated = []
 | |
|         for x, y in points:
 | |
|             dx = x - ox
 | |
|             dy = y - oy
 | |
|             rotated.append((ox + fc * dx - fs * dy, oy + fs * dx + fc * dy))
 | |
|         return rotated
 | |
| 
 | |
|     @staticmethod
 | |
|     def rhumb(lat1, lon1, lat2, lon2):
 | |
|         # Distance in m
 | |
|         lon1, lat1, lon2, lat2 = map(radians, [lon1, lat1, lon2, lat2])
 | |
|         dlon = lon2 - lon1
 | |
|         dlat = lat2 - lat1
 | |
|         mlat = (lat1 + lat2) / 2
 | |
|         return 6371000 * sqrt(dlat**2 + (cos(mlat) * dlon)**2)
 | |
| 
 | |
|     @staticmethod
 | |
|     def haversine(lat1, lon1, lat2, lon2):
 | |
|         # Great circle distance in nm
 | |
|         lon1, lat1, lon2, lat2 = map(radians, [lon1, lat1, lon2, lat2])
 | |
|         dlon = lon2 - lon1
 | |
|         dlat = lat2 - lat1
 | |
|         a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2
 | |
|         c = 2 * asin(sqrt(a))
 | |
|         return c * 3440
 | |
| 
 | |
|     def __init__(self, pageno, cfg, appdata, boatdata):
 | |
|         self.pageno = pageno
 | |
|         self.cfg = cfg
 | |
|         self.fullscreen = cfg['guistyle'] == 'fullscreen'
 | |
|         self.app = appdata
 | |
|         self.bd = boatdata
 | |
|         self.header = True
 | |
|         self.footer = True
 | |
|         self.hbled = False # Heartbeat LED 
 | |
|         self.hbfreq = 1000 # Heartbeat Frequenz in ms
 | |
|         self.keylock = False
 | |
|         self.icon = {}
 | |
|         self.icon['PREV'] = cairo.ImageSurface.create_from_png(os.path.join(cfg['imgpath'], "arrow_l1.png"))
 | |
|         self.icon['NEXT'] = cairo.ImageSurface.create_from_png(os.path.join(cfg['imgpath'], "arrow_r1.png"))
 | |
|         self.icon['UP'] = cairo.ImageSurface.create_from_png(os.path.join(cfg['imgpath'], "arrow_up.png"))
 | |
|         self.icon['DOWN'] = cairo.ImageSurface.create_from_png(os.path.join(cfg['imgpath'], "arrow_dn.png"))
 | |
|         self.icon['ILUM'] = cairo.ImageSurface.create_from_png(os.path.join(cfg['imgpath'], "lighton.png"))
 | |
|         self.sym_lock = cairo.ImageSurface.create_from_png(os.path.join(cfg['imgpath'], "lock.png"))
 | |
|         self.sym_swipe = cairo.ImageSurface.create_from_png(os.path.join(cfg['imgpath'], "swipe3.png"))
 | |
|         self.sym_exclamation = cairo.ImageSurface.create_from_png(os.path.join(cfg['imgpath'], "exclamation.png"))
 | |
|         self.sym_question = cairo.ImageSurface.create_from_png(os.path.join(cfg['imgpath'], "question.png"))
 | |
|         self.buttonlabel = {
 | |
|             1: '',
 | |
|             2: '',
 | |
|             3: '#PREV',
 | |
|             4: '#NEXT',
 | |
|             5: '',
 | |
|             6: '#ILUM'
 | |
|         }
 | |
| 
 | |
|     def handle_key(self, buttonid):
 | |
|         """
 | |
|         Diese Methode sollte in der Detailseite überladen werden
 | |
|         """
 | |
|         print(f"Button no. {buttonid} ignored")
 | |
|         return False
 | |
| 
 | |
|     def heartbeat(self, ctx):
 | |
|         """
 | |
|         Wie ausschalten bei Seitenwechsel?
 | |
|         """
 | |
|         ctx.save()
 | |
|         ctx.set_line_width(1)
 | |
|         ctx.set_source_rgb(0, 0, 0)
 | |
|         ctx.rectangle(201.5, 0.5, 23, 19)
 | |
|         if self.hbled:
 | |
|             ctx.fill_preserve()
 | |
|         ctx.stroke()
 | |
| 
 | |
|         if self.hbled:
 | |
|             ctx.set_source_rgb(0.86, 0.86, 0.86) # 0xdcdcdc
 | |
|         else:
 | |
|             ctx.set_source_rgb(0, 0, 0)
 | |
|         pno = str(self.pageno)
 | |
|         ext = ctx.text_extents(pno)
 | |
|         ctx.move_to(211 - ext.width / 2.0, 9 + ext.height / 2.0)
 | |
|         ctx.show_text(pno)
 | |
| 
 | |
|         ctx.stroke()
 | |
|         ctx.restore()
 | |
|         self.hbled = not self.hbled
 | |
| 
 | |
|     def display_new(self):
 | |
|         """
 | |
|         Aufruf jedenfalls bei Seitenwechsel.
 | |
|         Überladen in spezialisierten Seiten bei Bedarf
 | |
|         """
 | |
|         pass
 | |
| 
 | |
|     def draw_header(self, ctx):
 | |
|         """
 | |
|         Mögliche Zeichen für aktivierte Funktionen (max. 8)
 | |
|           AP - Accesspoint ist aktiv
 | |
|           WIFI - WIFI-Client
 | |
|           TCP
 | |
|           N2K - NMEA2000
 | |
|           183 
 | |
|           USB
 | |
|           GPS - GPS Fix vorhanden
 | |
|           TRK - Tracking aktiv
 | |
|           # TODO Umstellung auf Symbole je 16 Pixel zum Platz sparen
 | |
|           Neu: Nummer der aktiven Seite (1 - 10)
 | |
|         """
 | |
|         ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
 | |
|         ctx.set_font_size(16)
 | |
|         ctx.move_to(0.5, 14.5)
 | |
|         ctx.show_text(' '.join([s for s in self.app.status if self.app.status[s]]))
 | |
|         ctx.stroke()
 | |
| 
 | |
|         # Tastenstatus
 | |
|         ctx.save()
 | |
|         if self.keylock:
 | |
|             ctx.set_source_surface(self.sym_lock, 170, 1)
 | |
|         else:
 | |
|             ctx.set_source_surface(self.sym_swipe, 166, 1)
 | |
|         ctx.paint()
 | |
|         ctx.restore()
 | |
| 
 | |
|         # Heartbeat
 | |
|         self.heartbeat(ctx)
 | |
| 
 | |
|         # Datum und Uhrzeit
 | |
|         ctx.move_to(230, 14.5)
 | |
|         ctx.show_text(datetime.today().strftime('%H:%M %Y-%m-%d LOT'))
 | |
|         ctx.stroke()
 | |
| 
 | |
|     def draw_sidebuttons(self, ctx):
 | |
|         """
 | |
|         Seitliche Buttons zeichnen im Fullscreen-Modus
 | |
|         """
 | |
|         ctx.save()
 | |
|         ctx.select_font_face("AtariST8x16SystemFont")
 | |
|         ctx.set_font_size(16)
 | |
|         ctx.scale(1.6, 1.6)
 | |
|         ctx.set_source_rgb(0, 0, 0)
 | |
| 
 | |
|         bx = 400
 | |
|         by = (128, 192, 256, 320, 384, 448)
 | |
|         bw = 160
 | |
|         bh = 64
 | |
|         for i in range(6):
 | |
|             if len(self.buttonlabel[i+1]) > 0 :
 | |
|                 if self.buttonlabel[i+1][0] == "#":
 | |
|                     # Symbol verwenden 16x16 Pixel
 | |
|                     ctx.save()
 | |
|                     key = self.buttonlabel[i+1][1:]
 | |
|                     ctx.set_source_surface(self.icon[key], bx + bw / 3.2 - 8, by[i] / 1.6 - 6)
 | |
|                     ctx.paint()
 | |
|                     ctx.restore()
 | |
|                 else:
 | |
|                     w = ctx.text_extents(self.buttonlabel[i+1]).width
 | |
|                     ctx.move_to(bx + bw/3.2 - w/2, by[i] / 1.6 + 8)
 | |
|                     ctx.show_text(self.buttonlabel[i+1])
 | |
|         ctx.stroke()
 | |
|         ctx.restore()
 | |
| 
 | |
|     def draw_footer(self, ctx):
 | |
|         """
 | |
|         Nur Belegung der Buttons (label[1] bis label[6])
 | |
|         """
 | |
|         ctx.select_font_face("AtariST8x16SystemFont")
 | |
|         ctx.set_font_size(16)
 | |
|         x = (35, 101, 167, 233, 299, 365) 
 | |
|         y = 294
 | |
| 
 | |
|         ctx.set_line_width(1)
 | |
|         ctx.move_to(0, 280.5)
 | |
|         ctx.line_to(10, 280.5)
 | |
|         ctx.move_to(380, 280.5)
 | |
|         ctx.line_to(400, 280.5)
 | |
|         for i in range(5):
 | |
|             ctx.move_to(x[i]+32.5-10, 280.5)
 | |
|             ctx.line_to(x[i]+32.5+10, 280.5)
 | |
|             ctx.move_to(x[i]+32.5, 280)
 | |
|             ctx.line_to(x[i]+32.5, 300)
 | |
|         ctx.stroke()
 | |
| 
 | |
|         for i in range(6):
 | |
|             if len(self.buttonlabel[i+1]) > 0 :
 | |
|                 if self.buttonlabel[i+1][0] == "#":
 | |
|                     # Symbol verwenden 16x16 Pixel
 | |
|                     ctx.save()
 | |
|                     key = self.buttonlabel[i+1][1:]
 | |
|                     ctx.set_source_surface(self.icon[key], x[i]-8, y-13)
 | |
|                     ctx.paint()
 | |
|                     ctx.restore()
 | |
|                 else:
 | |
|                     text = self.buttonlabel[i+1]
 | |
|                     w = ctx.text_extents(text).width
 | |
|                     ctx.move_to(x[i] - w/2, y+2)               
 | |
|                     ctx.show_text(text)
 | |
|         ctx.stroke()
 | |
| 
 | |
|     def draw_query(self, ctx, title, message):
 | |
|         x, y, w, h = (50, 80, 300, 150)
 | |
|         ctx.save()
 | |
|         ctx.select_font_face("AtariST8x16SystemFont")
 | |
|         ctx.set_font_size(16)
 | |
|         # Dialogbox mit Rahmen
 | |
|         ctx.set_line_width(1)
 | |
|         ctx.set_source_rgb(*self.fgcolor)
 | |
|         ctx.rectangle(x + 0.5, y + 0.5, w, h)
 | |
|         ctx.stroke()
 | |
|         ctx.set_source_rgb(*self.bgcolor)
 | |
|         ctx.rectangle(x + 1, y + 1, w - 1, h - 1)
 | |
|         ctx.fill()
 | |
|         ctx.set_source_rgb(*self.fgcolor)
 | |
|         ctx.set_line_width(2)
 | |
|         ctx.rectangle(x + 3, y + 3, w - 5, h - 5)
 | |
|         ctx.clip_preserve()
 | |
|         ctx.stroke()
 | |
|         # Symbol
 | |
|         ctx.save()
 | |
|         ctx.set_source_surface(self.sym_question, x + 16, y + 16)
 | |
|         ctx.paint()
 | |
|         ctx.restore()
 | |
|         # Titel
 | |
|         ctx.move_to(x + 64, y + 30)
 | |
|         ctx.show_text(title)
 | |
|         ctx.move_to(x + 64, y + 48)
 | |
|         # Meldung, umgebrochen
 | |
|         n = 0
 | |
|         for l in wordwrap(message,  (w - 16 - 8) / 8):
 | |
|             ctx.move_to(x + 16, y + 70 + n)
 | |
|             ctx.show_text(l)
 | |
|             n += 16
 | |
|             if n > 64:
 | |
|                 break
 | |
|         # Auswahlmöglichkeiten
 | |
|         self.draw_button(ctx, x + 16, y + h - 32, "YES")
 | |
|         self.draw_button(ctx, x + w - 96, y + h - 32, "NO")
 | |
|         ctx.restore()
 | |
| 
 | |
|     def draw_alarm(self, ctx, source, alarmid, message):
 | |
|         x, y, w, h = (50, 100, 300, 150)
 | |
|         ctx.select_font_face("AtariST8x16SystemFont")
 | |
|         ctx.set_font_size(16)
 | |
|         # Dialogbox mit Rahmen
 | |
|         ctx.set_line_width(1)
 | |
|         ctx.set_source_rgb(*self.fgcolor)
 | |
|         ctx.rectangle(x + 0.5, y + 0.5, w, h)
 | |
|         ctx.stroke()
 | |
|         ctx.set_source_rgb(*self.bgcolor)
 | |
|         ctx.rectangle(x + 1, y + 1, w - 1, h - 1)
 | |
|         ctx.fill()
 | |
|         ctx.set_source_rgb(*self.fgcolor)
 | |
|         ctx.set_line_width(2)
 | |
|         ctx.rectangle(x + 3, y + 3, w - 5, h - 5)
 | |
|         ctx.clip_preserve()
 | |
|         ctx.stroke()
 | |
|         # Symbol
 | |
|         ctx.save()
 | |
|         ctx.set_source_surface(self.sym_exclamation, x + 16, y + 16)
 | |
|         ctx.paint()
 | |
|         ctx.restore()
 | |
|         # Titel
 | |
|         ctx.move_to(x + 64, y + 30)
 | |
|         ctx.show_text("A L A R M")
 | |
|         ctx.move_to(x + 64, y + 48)
 | |
|         ctx.show_text(f"#{alarmid} from {source}")
 | |
|         # Alarmmeldung, umgebrochen
 | |
|         n = 0
 | |
|         for l in wordwrap(message,  (w - 16 - 8) / 8):
 | |
|             ctx.move_to(x + 16, y + 80 + n)
 | |
|             ctx.show_text(l)
 | |
|             n += 16
 | |
|             if n > 64:
 | |
|                 break
 | |
|         # Button-Hinweis
 | |
|         self.draw_text_center(ctx, x + w / 2, y + h - 16, "Press button 1 to dismiss alarm")
 | |
| 
 | |
|     def clear(self):
 | |
|         ctx.set_source_rgb(1, 1, 1)
 | |
|         ctx.rectangle(0, 0, 399, 299)
 | |
|         ctx.fill()
 | |
|         ctx.set_source_rgb(0, 0, 0)
 | |
| 
 | |
|     def draw_text_center(self, ctx, x, y, content, rotate=False, baseline=False, fill=False, fix1=False):
 | |
|         """
 | |
|         Korrektur für DSEG7: Die Breite der 1 ist gleich 0.289 * Breite der anderen Ziffern
 | |
|         Da der Leerraum bei der Ausgabe mit berücksichtigt wird, muß die tatsächliche
 | |
|         Ausgabeposition links von der Mitte sein um (1 - 0.289) * Breite (=0.711)
 | |
|         Zusätzlich muß der Abstand zwischen der 1 und dem nachfolgenden Zeichen berücksichtigt
 | |
|         werden
 | |
|         """
 | |
|         if fix1 and content[0] == '1':
 | |
|             print("Fix1")
 | |
|             ext1 = ctx.text_extents('1')
 | |
|             w1 = 0.289 * ext1.width
 | |
|             dx = ext1.width - w1
 | |
|             ext = ctx.text_extents(content[1:])
 | |
|         else:
 | |
|             ext = ctx.text_extents(content)
 | |
|         if fill:
 | |
|             ctx.set_source_rgb(*self.bgcolor)
 | |
|             xf = x + ext.x_bearing - 2
 | |
|             yf = y + ext.height / 2 + ext.y_bearing - 2
 | |
|             wf = ext.width + 4
 | |
|             if fix1:
 | |
|                 wf += w1
 | |
|             hf = ext.height + 4
 | |
|             ctx.rectangle(xf, yf, wf, hf)
 | |
|             ctx.fill()
 | |
|         ctx.set_source_rgb(*self.fgcolor)
 | |
|         if rotate:
 | |
|             w = ext[2]
 | |
|             if fix1 and content[0] == '1':
 | |
|                 w += w1
 | |
|                 x = x - dx
 | |
|             if baseline:
 | |
|                 ctx.move_to(x - w / 2.0, y)
 | |
|             else:
 | |
|                 ctx.move_to(x - w / 2.0, y + ext[2] / 2.0)
 | |
|             ctx.save()
 | |
|             ctx.rotate(1.5 * math.pi)
 | |
|             ctx.show_text(content)
 | |
|             ctx.restore()
 | |
|         else:
 | |
|             w = ext.width
 | |
|             if fix1 and content[0] == '1':
 | |
|                 w += w1
 | |
|                 x = x - dx
 | |
|             if baseline:
 | |
|                 ctx.move_to(x - w / 2.0, y)
 | |
|             else:
 | |
|                 ctx.move_to(x - w / 2.0, y + ext[3] / 2.0)
 | |
|             ctx.show_text(content)
 | |
|         ctx.stroke()
 | |
| 
 | |
|     def draw_text_ralign(self, ctx, x, y, content, fix1=False):
 | |
|         if fix1 and content[0] == '1':
 | |
|             w1 = ctx.text_extents('1')[2] * 0.289
 | |
|             ext = ctx.text_extents(content[1:])
 | |
|             w = ext[2] + w1
 | |
|             x = x - dx
 | |
|         else:
 | |
|             ext = ctx.text_extents(content)
 | |
|             w = ext[2]
 | |
|         ctx.move_to(x - w, y)
 | |
|         ctx.show_text(content)
 | |
|         ctx.stroke()
 | |
| 
 | |
|     def draw_text_boxed(self, ctx, x, y, w, h, content, inverted=False, border=False):
 | |
|         ctx.save()
 | |
|         ctx.set_line_width(1)
 | |
|         # Background fill
 | |
|         ctx.set_source_rgb(*self.fgcolor)
 | |
|         if inverted:
 | |
|             ctx.rectangle(x, y + 0.5, w, h)
 | |
|             ctx.fill()
 | |
|         else:
 | |
|             if border:
 | |
|                 ctx.rectangle(x + 0.5, y + 0.5, w, h)
 | |
|                 ctx.stroke()
 | |
|         # Text
 | |
|         if inverted:
 | |
|             ctx.set_source_rgb(*self.bgcolor)
 | |
|         else:
 | |
|             ctx.set_source_rgb(*self.fgcolor)
 | |
|         ctx.move_to(x + 4, y + h - 5 + 0.5)
 | |
|         ctx.show_text(content)
 | |
|         ctx.stroke()
 | |
|         ctx.restore()
 | |
| 
 | |
|     def draw_button(self, ctx, x, y, label):
 | |
|         ctx.save()
 | |
|         ctx.select_font_face("AtariST8x16SystemFont")
 | |
|         ctx.set_font_size(16)
 | |
|         ctx.set_line_width(3)
 | |
|         ctx.set_source_rgb(*self.fgcolor)
 | |
|         ext = ctx.text_extents(label)
 | |
|         w = max(ext.width, 80)
 | |
|         h = 24
 | |
|         ctx.rectangle(x + 0.5, y + 0.5, w, h)
 | |
|         ctx.stroke()
 | |
|         ctx.move_to(x + (w - ext.width) / 2.0, y + h - 5)
 | |
|         ctx.show_text(label)
 | |
|         ctx.restore()
 | |
| 
 | |
| def wordwrap(text, wrap):
 | |
|     # Wrap long line to multiple lines, monospaced character set
 | |
|     # e.g. used for alarm dialog boxes
 | |
|     llen = 0
 | |
|     l = 0
 | |
|     lines = [""]
 | |
|     for w in text.split():
 | |
|         wordlength = len(w)
 | |
|         if llen + 1 + wordlength <= wrap:
 | |
|             if llen > 0:
 | |
|                 lines[l] += ' '
 | |
|             lines[l] += w
 | |
|             llen += wordlength + 1
 | |
|         else:
 | |
|             lines.append(w)
 | |
|             l += 1
 | |
|             llen = wordlength
 | |
|     return lines
 |