#
# Copyright 2021-2024 konawasabi
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
'''
'''
import sys
import pathlib
import os
import webbrowser
import argparse
import time
import tkinter as tk
from tkinter import ttk
import tkinter.filedialog as filedialog
import tkinter.simpledialog as simpledialog
import tkinter.font as font
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib import rcParams
import matplotlib.gridspec
from PIL import Image
import numpy as np
rcParams['font.family'] = 'sans-serif'
rcParams['font.sans-serif'] = ['Hiragino Sans', 'Yu Gothic', 'Meirio', 'Takao', 'IPAexGothic', 'IPAPGothic', 'VL PGothic', 'Noto Sans CJK JP']
from . import track_control
from . import drawcursor
from . import backimg
from . import measure
from ._version import __version__
from . import trackwindow
from . import heightwindow
from . import kp_handling
from . import mediantrack
[docs]
class Catcher: # tkinter内で起きた例外をキャッチする
def __init__(self, func, subst, widget):
self.func = func
self.subst = subst
self.widget = widget
def __call__(self, *args):
try:
if self.subst:
args = self.subst(*args)
return self.func(*args)
except Exception as e:
if not __debug__: # デバッグモード(-O)なら素通し。pdbが起動する
raise e
else:
print(e) # 通常モードならダイアログ表示
tk.messagebox.showinfo(message=e)
[docs]
class mainwindow(ttk.Frame):
def __init__(self, master):
self.parent = master
super().__init__(master, padding='3 3 3 3')
self.master.title('Tsutsuji trackcomputer ver. {:s}'.format(__version__))
self.grid(column=0, row=0, sticky=(tk.N, tk.W, tk.E, tk.S))
self.master.columnconfigure(0, weight=1)
self.master.rowconfigure(0, weight=1)
master.protocol('WM_DELETE_WINDOW', self.ask_quit)
self.backimgctrl = backimg.BackImgControl(self)
self.cursor = drawcursor.cursor(self)
self.measurewindow = measure.interface(self)
self.trackwindow = trackwindow.TrackWindow(self)
self.trackcontrol = track_control.TrackControl()
self.staticmapctrl = backimg.TileMapControl(self)
self.heightwindow = heightwindow.HeightWindow(self)
self.create_widgets()
self.create_menubar()
self.bind_keyevent()
[docs]
def create_widgets(self):
font_title = font.Font(weight='bold',size=10)
# プロットフレーム
self.canvas_frame = ttk.Frame(self, padding='3 3 3 3')
self.canvas_frame.grid(column=0, row=0, sticky=(tk.N, tk.W, tk.E, tk.S))
self.fig_plane = plt.figure(figsize=(9,7),tight_layout=True)
gs1 = self.fig_plane.add_gridspec(nrows=1,ncols=1)
self.ax_plane = self.fig_plane.add_subplot(gs1[0])
self.plt_canvas_base = tk.Canvas(self.canvas_frame, bg="white", width=900, height=700)
self.plt_canvas_base.grid(row = 0, column = 0, sticky=(tk.N, tk.W, tk.E, tk.S))
def on_canvas_resize(event):
#print(event, self.fig_plane.get_size_inches())
self.plt_canvas_base.itemconfigure(self.fig_frame_id, width=event.width, height=event.height)
self.fig_frame = tk.Frame(self.plt_canvas_base)
self.fig_frame_id = self.plt_canvas_base.create_window((0, 0), window=self.fig_frame, anchor="nw")
self.fig_frame.columnconfigure(0, weight=1)
self.fig_frame.rowconfigure(0, weight=1)
self.plt_canvas_base.bind("<Configure>", on_canvas_resize)
self.fig_canvas = FigureCanvasTkAgg(self.fig_plane, master=self.fig_frame)
self.fig_canvas.draw()
self.fig_canvas.get_tk_widget().grid(row=0, column=0, sticky=(tk.N, tk.W, tk.E, tk.S))
self.canvas_frame.columnconfigure(0, weight=1)
#self.canvas_frame.columnconfigure(1, weight=1)
self.canvas_frame.rowconfigure(0, weight=1)
#self.canvas_frame.rowconfigure(1, weight=1)
#ボタンフレーム
self.button_frame = ttk.Frame(self, padding='3 3 3 3')
self.button_frame.grid(column=1, row=0, sticky=(tk.N, tk.W, tk.E, tk.S))
# ---
self.replot_btn = ttk.Button(self.button_frame, text="Replot", command = self.drawall)
self.replot_btn.grid(column=0, row=0, sticky=(tk.N, tk.W, tk.E))
self.plotarea_frame = ttk.Labelframe(self.button_frame, padding='3 3 3 3', text = 'Plot control')
self.plotarea_frame.grid(column=0, row=1, sticky=(tk.N, tk.W, tk.E, tk.S))
self.plotarea_val_frame = ttk.Frame(self.plotarea_frame, padding='3 3 3 3')
self.plotarea_val_frame.grid(column=0, row=0, sticky=(tk.N, tk.W, tk.E, tk.S))
self.viewpos_v = [tk.DoubleVar(value=0),tk.DoubleVar(value=0)]
self.viewp_scale_v = tk.DoubleVar(value=1000)
self.view_whole_v = tk.StringVar()
self.view_whole_v.set('False')
self.aspectratio_v = tk.DoubleVar(value=1)
self.viewp_x_l = ttk.Label(self.plotarea_val_frame, text='x')
self.viewp_y_l = ttk.Label(self.plotarea_val_frame, text='y')
self.viewp_sc_l = ttk.Label(self.plotarea_val_frame, text='scale')
self.viewp_asr_l = ttk.Label(self.plotarea_val_frame, text='Y mag.')
self.viewp_x_l.grid(column=0, row=0, sticky=(tk.E,tk.W))
self.viewp_y_l.grid(column=2, row=0, sticky=(tk.E,tk.W))
self.viewp_sc_l.grid(column=0, row=1, sticky=(tk.E,tk.W))
self.viewp_asr_l.grid(column=2, row=1, sticky=(tk.E,tk.W))
self.viewp_x_e = ttk.Entry(self.plotarea_val_frame, textvariable=self.viewpos_v[0],width=5)
self.viewp_y_e = ttk.Entry(self.plotarea_val_frame, textvariable=self.viewpos_v[1],width=5)
self.viewp_sc_e = ttk.Entry(self.plotarea_val_frame, textvariable=self.viewp_scale_v,width=5)
self.view_whole_e = ttk.Checkbutton(self.plotarea_val_frame, text='Whole', variable=self.view_whole_v, onvalue='True', offvalue='False')
self.viewp_asr_e = ttk.Entry(self.plotarea_val_frame, textvariable=self.aspectratio_v,width=5)
self.viewp_x_e.grid(column=1, row=0, sticky=(tk.E,tk.W))
self.viewp_y_e.grid(column=3, row=0, sticky=(tk.E,tk.W))
self.viewp_sc_e.grid(column=1, row=1, sticky=(tk.E,tk.W))
self.viewp_asr_e.grid(column=3, row=1, sticky=(tk.E,tk.W))
self.view_whole_e.grid(column=0, row=3, sticky=(tk.E,tk.W))
# ---
self.plotmove_frame = ttk.Frame(self.plotarea_frame, padding='3 3 3 3')
self.plotmove_frame.grid(column=0, row=1, sticky=(tk.N, tk.W, tk.E, tk.S))
self.plotmove_btn_up = ttk.Button(self.plotmove_frame, text="↑", command = lambda: self.move_xy(0,-1))
self.plotmove_btn_down = ttk.Button(self.plotmove_frame, text="↓", command = lambda: self.move_xy(0,1))
self.plotmove_btn_left = ttk.Button(self.plotmove_frame, text="←", command = lambda: self.move_xy(-1,0))
self.plotmove_btn_right = ttk.Button(self.plotmove_frame, text="→", command = lambda: self.move_xy(1,0))
self.plotmove_btn_up.grid(column=1, row=0, sticky=(tk.E,tk.W))
self.plotmove_btn_down.grid(column=1, row=2, sticky=(tk.E,tk.W))
self.plotmove_btn_left.grid(column=0, row=1, sticky=(tk.E,tk.W))
self.plotmove_btn_right.grid(column=2, row=1, sticky=(tk.E,tk.W))
# ---
self.plotarea_symbol_frame = ttk.Labelframe(self.plotarea_frame, padding='3 3 3 3', text = 'Symbols')
self.plotarea_symbol_frame.grid(column=0, row=2, sticky=(tk.E,tk.W))
self.plot_marker_ctrl = {}
position = 0
for val in ['radius','gradient','supplemental_cp','track']:
self.plot_marker_ctrl[val] = {}
self.plot_marker_ctrl[val]['variable'] = tk.BooleanVar(value=False)
self.plot_marker_ctrl[val]['widget'] = ttk.Checkbutton(self.plotarea_symbol_frame, text=val, variable=self.plot_marker_ctrl[val]['variable'], onvalue=True, offvalue=False)
self.plot_marker_ctrl[val]['widget'].grid(column=0, row=position, sticky=(tk.E,tk.W))
position +=1
# ---
self.measure_btn = ttk.Button(self.button_frame, text="Measure", command = self.measure)
self.measure_btn.grid(column=0, row=2, sticky=(tk.N, tk.W, tk.E, tk.S))
if not __debug__:
self.printtracks_btn = ttk.Button(self.button_frame, text="P. Tracks", command = self.trackcontrol.dump_trackdata)
self.printtracks_btn.grid(column=0, row=4, sticky=(tk.N, tk.W, tk.E))
self.printpos_btn = ttk.Button(self.button_frame, text="P. Pos", command = self.draw_tracks_cp)
self.printpos_btn.grid(column=0, row=5, sticky=(tk.N, tk.W, tk.E))
self.othertrack_btn = ttk.Button(self.button_frame, text="OtherTrack", command = self.get_othertrack)
self.othertrack_btn.grid(column=0, row=6, sticky=(tk.N, tk.W, tk.E))
'''
self.statmap1_btn = ttk.Button(self.button_frame, text="statmap1", command = self.getmaptile)
self.statmap1_btn.grid(column=0, row=7, sticky=(tk.N, tk.W, tk.E))
'''
self.getrelrad_btn = ttk.Button(self.button_frame, text="Generate", command = self.generate_output)
self.getrelrad_btn.grid(column=0, row=10, sticky=(tk.E, tk.W, tk.S))
self.button_frame.rowconfigure(10, weight=1)
# ウィンドウリサイズに対する設定
self.columnconfigure(0, weight=1)
#self.columnconfigure(1, weight=1)
self.rowconfigure(0, weight=1)
# プロットウィンドウ描画
self.drawall()
[docs]
def create_menubar(self):
self.master.option_add('*tearOff', False)
self.menubar = tk.Menu(self.master)
self.menu_file = tk.Menu(self.menubar)
self.menu_compute = tk.Menu(self.menubar)
self.menu_height = tk.Menu(self.menubar)
self.menu_option = tk.Menu(self.menubar)
self.menu_help = tk.Menu(self.menubar)
self.menubar.add_cascade(menu=self.menu_file, label='ファイル')
self.menubar.add_cascade(menu=self.menu_compute, label='メイン処理')
self.menubar.add_cascade(menu=self.menu_height, label='高度')
self.menubar.add_cascade(menu=self.menu_option, label='オプション')
self.menubar.add_cascade(menu=self.menu_help, label='ヘルプ')
self.menu_file.add_command(label='開く...', command=self.opencfg, accelerator='Control+O')
self.menu_file.add_command(label='リロード', command=self.reloadcfg, accelerator='F5')
self.menu_file.add_separator()
self.menu_file.add_command(label='終了', command=self.ask_quit, accelerator='Alt+F4')
self.menu_compute.add_command(label='Measure...', command=self.measure, accelerator='Control+M')
self.menu_compute.add_command(label='Generate', command=self.generate_output, accelerator='Control+G')
self.menu_compute.add_separator()
self.menu_compute.add_command(label='Replot', command=self.drawall, accelerator='Return')
self.heightwindow.create_menu()
self.menu_option.add_command(label='Backimg...', command=self.backimgctrl.create_window)
self.menu_option.add_command(label='Load Backimg...', command=self.backimgctrl.load_setting)
self.menu_option.add_command(label='Save Backimg...', command=self.backimgctrl.save_setting)
self.menu_option.add_separator()
self.menu_option.add_command(label='Maptile...', command=self.staticmapctrl.create_paramwindow, accelerator='Control+T')
self.menu_option.add_command(label='Refresh Maptile', command=self.getmaptile, accelerator='Shift+Return')
self.menu_option.add_command(label='Export Maptile...', command=self.staticmapctrl.export)
self.menu_option.add_separator()
self.menu_option.add_command(label='Track...', command=self.trackwindow.create_window)
self.menu_option.add_separator()
self.menu_option.add_command(label='Handling kiloposts...', command = lambda: kp_handling.GUI(self))
self.menu_option.add_separator()
self.menu_option.add_command(label='Mediantrack...', command = lambda: mediantrack.GUI(self))
self.menu_help.add_command(label='ヘルプ...', command=self.open_webdocument)
self.menu_help.add_command(label='Tsutsujiについて...', command=self.aboutwindow)
self.master['menu'] = self.menubar
[docs]
def bind_keyevent(self):
self.master.bind("<Control-o>", self.opencfg)
self.master.bind("<Control-m>", self.measure)
self.master.bind("<Control-g>", self.generate_output)
self.master.bind("<Control-t>", self.staticmapctrl.create_paramwindow)
self.master.bind("<Shift-Return>", self.getmaptile)
self.master.bind("<F5>", self.reloadcfg)
self.master.bind("<Alt-F4>", self.ask_quit)
self.master.bind("<Return>", self.press_return)
self.master.bind("<Shift-Left>", self.press_arrowkey)
self.master.bind("<Shift-Right>", self.press_arrowkey)
self.master.bind("<Shift-Up>", self.press_arrowkey)
self.master.bind("<Shift-Down>", self.press_arrowkey)
[docs]
def ask_quit(self, event=None, ask=True):
if ask:
if tk.messagebox.askyesno(message='Tsutsuji を終了しますか?'):
self.quit()
else:
self.quit()
[docs]
def opencfg(self, event=None, in_dir=None):
inputdir = filedialog.askopenfilename() if in_dir == None else in_dir
print('loading',inputdir)
self.trackcontrol.loadcfg(inputdir)
self.trackcontrol.loadmap()
if self.trackcontrol.conf.general['backimg'] is not None:
self.backimgctrl.load_setting(path = self.trackcontrol.conf.general['backimg'])
elif self.backimgctrl.conf_path is not None:
self.backimgctrl.load_setting(path = self.backimgctrl.conf_path)
if self.trackcontrol.conf.general['backimg_height'] is not None:
self.heightwindow.backimg.load_setting(path = self.trackcontrol.conf.general['backimg_height'])
elif self.heightwindow.backimg.conf_path is not None:
self.heightwindow.backimg.load_setting(path = self.heightwindow.backimg.conf_path)
self.trackwindow.reset_treevalue()
self.measurewindow.reload_trackkeys()
self.heightwindow.reloadcfg()
plotsize = self.fig_plane.get_size_inches()
self.staticmapctrl.setparams_fromcfg(self.trackcontrol.conf.maptile)
self.staticmapctrl.getimg(self.viewp_scale_v.get(),plotsize[1]/plotsize[0])
self.drawall()
[docs]
def reloadcfg(self, event=None):
if self.trackcontrol.path is not None:
self.opencfg(event=event,in_dir=self.trackcontrol.path)
[docs]
def draw2dplot(self):
self.ax_plane.cla()
self.trackcontrol.plot2d(self.ax_plane)
self.fig_canvas.draw()
[docs]
def drawall(self):
self.ax_plane.cla()
self.trackcontrol.plot2d(self.ax_plane)
for key in self.plot_marker_ctrl.keys():
if self.plot_marker_ctrl[key]['variable'].get():
self.trackcontrol.plot_symbols(self.ax_plane,key)
self.measurewindow.drawall()
plotsize = self.fig_plane.get_size_inches()
if self.view_whole_v.get() == 'True':
imgarea = self.backimgctrl.imgsarea()
imgarea = self.trackcontrol.drawarea(imgarea)
self.ax_plane.set_xlim(imgarea[0],imgarea[1])
self.ax_plane.set_ylim(imgarea[2],imgarea[3])
else:
center = [self.viewpos_v[0].get(),self.viewpos_v[1].get()]
windowratio = 1/self.aspectratio_v.get()*plotsize[1]/plotsize[0] # 平面図のアスペクト比を取得
scalex = self.viewp_scale_v.get()
scaley = windowratio * scalex
self.ax_plane.set_xlim(center[0]-scalex/2, center[0]+scalex/2)
self.ax_plane.set_ylim(center[1]-scaley/2, center[1]+scaley/2)
self.staticmapctrl.showimg(self.ax_plane,as_ratio=plotsize[1]/plotsize[0],ymag=self.aspectratio_v.get())
for i in self.backimgctrl.imgs.keys():
self.backimgctrl.imgs[i].show(self.ax_plane,as_ratio=plotsize[1]/plotsize[0],ymag=self.aspectratio_v.get())
self.ax_plane.invert_yaxis()
self.fig_canvas.draw()
self.heightwindow.drawall()
[docs]
def move_xy(self,x,y):
nowpos = [self.viewpos_v[0].get(),self.viewpos_v[1].get()]
plotsize = self.fig_plane.get_size_inches()
windowratio = 1/self.aspectratio_v.get()*plotsize[1]/plotsize[0]
scalex = self.viewp_scale_v.get()
scaley = windowratio * scalex
self.viewpos_v[0].set(nowpos[0] + x*scalex/5)
self.viewpos_v[1].set(nowpos[1] + y*scaley/5)
self.drawall()
[docs]
def measure(self, event=None):
self.measurewindow.create_widgets()
[docs]
def draw_tracks_cp(self):
self.trackcontrol.plot_controlpoints(self.ax_plane)
self.fig_canvas.draw()
[docs]
def generate_output(self, event=None):
if not __debug__:
start = time.time()
self.trackcontrol.generate_mapdata()
if not __debug__:
print(time.time()-start)
self.get_othertrack()
[docs]
def aboutwindow(self, event=None):
msg = 'Tsutsuji trackcomputer\n'
msg += 'Version '+__version__+'\n\n'
msg += 'Copyright © 2024 konawasabi\n'
msg += 'Released under the Apache License, Version 2.0 .\n'
msg += 'https://www.apache.org/licenses/LICENSE-2.0'
tk.messagebox.showinfo(message=msg)
[docs]
def open_webdocument(self, event=None):
webbrowser.open('https://konawasabi.github.io/tsutsuji-trackcomputer/')
[docs]
def sendtopmost(self,event=None):
self.master.lift()
self.master.focus_force()
[docs]
def get_othertrack(self, event=None):
self.trackcontrol.generate_otdata()
self.trackwindow.reset_treevalue()
self.drawall()
[docs]
def press_return(self, event=None):
self.drawall()
self.master.focus_force()
[docs]
def press_arrowkey(self, event=None):
if event.keysym == 'Left':
self.move_xy(-1,0)
elif event.keysym == 'Right':
self.move_xy(1,0)
elif event.keysym == 'Up':
self.move_xy(0,-1)
elif event.keysym == 'Down':
self.move_xy(0,1)
[docs]
def getmaptile(self, event=None):
plotsize = self.fig_plane.get_size_inches()
self.staticmapctrl.getimg(self.viewp_scale_v.get(),plotsize[1]/plotsize[0])
self.drawall()
[docs]
def main():
if not __debug__:
# エラーが発生した場合、デバッガを起動 https://gist.github.com/podhmo/5964702e7471ccaba969105468291efa
def info(type, value, tb):
if hasattr(sys, "ps1") or not sys.stderr.isatty():
# You are in interactive mode or don't have a tty-like
# device, so call the default hook
sys.__excepthook__(type, value, tb)
else:
import traceback, pdb
# You are NOT in interactive mode; print the exception...
traceback.print_exception(type, value, tb)
# ...then start the debugger in post-mortem mode
pdb.pm()
sys.excepthook = info
print('Debug mode')
argparser = argparse.ArgumentParser()
argparser.add_argument('filepath', metavar='F', type=str, help='input cfg file', nargs='?')
argparser.add_argument('-n', '--nogui', help='no gui mode', action='store_true')
args = argparser.parse_args()
if args.nogui:
if __debug__:
def errorcatcher(type, value, tb):
#print(type, value, tb,file=sys.stderr)
print(value, file=sys.stderr)
exit
sys.excepthook = errorcatcher
if args.filepath is None:
raise Exception('no cfg file')
trackcontrol = track_control.TrackControl()
trackcontrol.loadcfg(args.filepath)
trackcontrol.loadmap()
trackcontrol.generate_mapdata()
else:
tk.CallWrapper = Catcher
root = tk.Tk()
app = mainwindow(master=root)
#if len(sys.argv)>1:
# app.opencfg(in_dir=sys.argv[1])
if args.filepath is not None:
app.opencfg(in_dir=args.filepath)
app.mainloop()