class GcodeViewerScreen(Screen): current_z = NumericProperty(0) select_mode = BooleanProperty(False) twod_mode = BooleanProperty(False) laser_mode = BooleanProperty(False) valid = BooleanProperty(False) def __init__(self, comms=None, **kwargs): super(GcodeViewerScreen, self).__init__(**kwargs) self.app = App.get_running_app() self.last_file_pos = None self.canv = InstructionGroup() self.bind(pos=self._redraw, size=self._redraw) self.last_target_layer = 0 self.tx = 0 self.ty = 0 self.scale = 1.0 self.comms = comms self.twod_mode = self.app.is_cnc self.rval = 0.0 def loading(self, ll=1): self.valid = False self.li = Image(source='img/image-loading.gif') self.add_widget(self.li) self.ids.surface.canvas.remove(self.canv) threading.Thread(target=self._load_file, args=(ll, )).start() @mainthread def _loaded(self): Logger.debug("GcodeViewerScreen: in _loaded. ok: {}".format( self._loaded_ok)) self.remove_widget(self.li) self.li = None self.ids.surface.canvas.add(self.canv) self.valid = self._loaded_ok if self._loaded_ok: # not sure why we need to do this self.ids.surface.top = Window.height if self.app.is_connected: self.app.bind(wpos=self.update_tool) def _load_file(self, ll): self._loaded_ok = False try: self.parse_gcode_file(self.app.gcode_file, ll, True) except Exception: print(traceback.format_exc()) mb = MessageBox( text='File not found: {}'.format(self.app.gcode_file)) mb.open() self._loaded() def _redraw(self, instance, value): self.ids.surface.canvas.remove(self.canv) self.ids.surface.canvas.add(self.canv) def clear(self): self.app.unbind(wpos=self.update_tool) if self.li: self.remove_widget(self.li) self.li = None if self.select_mode: self.stop_cursor(0, 0) self.select_mode = False self.ids.select_mode_but.state = 'normal' self.valid = False self.is_visible = False self.canv.clear() self.ids.surface.canvas.remove(self.canv) self.last_target_layer = 0 # reset scale and translation m = Matrix() m.identity() self.ids.surface.transform = m # not sure why we need to do this self.ids.surface.top = Window.height def next_layer(self): self.loading(self.last_target_layer + 1) def prev_layer(self): n = 1 if self.last_target_layer <= 1 else self.last_target_layer - 1 self.loading(n) def print(self): self.app.main_window._start_print() # ---------------------------------------------------------------------- # Return center x,y,z,r for arc motions 2,3 and set self.rval # Cribbed from bCNC # ---------------------------------------------------------------------- def motionCenter(self, gcode, plane, xyz_cur, xyz_val, ival, jval, kval=0.0): if self.rval > 0.0: if plane == XY: x = xyz_cur[0] y = xyz_cur[1] xv = xyz_val[0] yv = xyz_val[1] elif plane == XZ: x = xyz_cur[0] y = xyz_cur[2] xv = xyz_val[0] yv = xyz_val[2] else: x = xyz_cur[1] y = xyz_cur[2] xv = xyz_val[1] yv = xyz_val[2] ABx = xv - x ABy = yv - y Cx = 0.5 * (x + xv) Cy = 0.5 * (y + yv) AB = math.sqrt(ABx**2 + ABy**2) try: OC = math.sqrt(self.rval**2 - AB**2 / 4.0) except Exception: OC = 0.0 if gcode == 2: OC = -OC # CW if AB != 0.0: return Cx - OC * ABy / AB, Cy + OC * ABx / AB else: # Error!!! return x, y else: # Center xc = xyz_cur[0] + ival yc = xyz_cur[1] + jval zc = xyz_cur[2] + kval self.rval = math.sqrt(ival**2 + jval**2 + kval**2) if plane == XY: return xc, yc elif plane == XZ: return xc, zc else: return yc, zc extract_gcode = re.compile(r"(G|X|Y|Z|I|J|K|E|S)(-?\d*\.?\d*\.?)") def parse_gcode_file(self, fn, target_layer=0, one_layer=False): # open file parse gcode and draw Logger.debug("GcodeViewerScreen: parsing file {}".format(fn)) lastpos = [self.app.wpos[0], self.app.wpos[1], -1] # XYZ, set to initial tool position lastz = None lastdeltaz = None laste = 0 lasts = 1 layer = -1 last_gcode = -1 points = [] max_x = float('nan') max_y = float('nan') min_x = float('nan') min_y = float('nan') has_e = False plane = XY rel_move = False self.is_visible = True if self.laser_mode: self.twod_mode = True # laser mode implies 2D mode self.last_target_layer = target_layer # reset scale and translation m = Matrix() m.identity() self.ids.surface.transform = m # remove all instructions from canvas self.canv.clear() self.canv.add(PushMatrix()) modal_g = 0 cnt = 0 found_layer = False x = lastpos[0] y = lastpos[1] z = lastpos[2] with open(fn) as f: # if self.last_file_pos: # # jump to last read position # f.seek(self.last_file_pos) # self.last_file_pos= None # print('Jumped to Saved position: {}'.format(self.last_file_pos)) for ln in f: cnt += 1 ln = ln.strip() if not ln: continue if ln.startswith(';'): continue if ln.startswith('('): continue p = ln.find(';') if p >= 0: ln = ln[:p] matches = self.extract_gcode.findall(ln) # this handles multiple G codes on one line gcodes = [] d = {} for m in matches: #print(m) if m[0] == 'G' and 'G' in d: # we have another G code on the same line gcodes.append(d) d = {} d[m[0]] = float(m[1]) gcodes.append(d) for d in gcodes: if not d: continue Logger.debug("GcodeViewerScreen: d={}".format(d)) # handle modal commands if 'G' not in d and ('X' in d or 'Y' in d or 'Z' in d or 'S' in d): d['G'] = modal_g gcode = int(d['G']) # G92 E0 resets E if 'G' in d and gcode == 92 and 'E' in d: laste = float(d['E']) has_e = True if 'G' in d and (gcode == 91 or gcode == 90): rel_move = gcode == 91 # only deal with G0/1/2/3 if gcode > 3: continue modal_g = gcode # see if it is 3d printing (ie has an E axis on a G1) if not has_e and ('E' in d and 'G' in d and gcode == 1): has_e = True if rel_move: x += 0 if 'X' not in d else float(d['X']) y += 0 if 'Y' not in d else float(d['Y']) z += 0 if 'Z' not in d else float(d['Z']) else: x = lastpos[0] if 'X' not in d else float(d['X']) y = lastpos[1] if 'Y' not in d else float(d['Y']) z = lastpos[2] if 'Z' not in d else float(d['Z']) i = 0.0 if 'I' not in d else float(d['I']) j = 0.0 if 'J' not in d else float(d['J']) self.rval = 0.0 if 'R' not in d else float(d['R']) e = laste if 'E' not in d else float(d['E']) s = lasts if 'S' not in d else float(d['S']) if not self.twod_mode: # handle layers (when Z changes) if z == -1: # no z seen yet layer = -1 continue if lastz is None: # first layer lastz = z layer = 1 if z != lastz: # count layers layer += 1 lastz = z # wait until we get to the requested layer if layer != target_layer: lastpos[2] = z continue if layer > target_layer and one_layer: # FIXME for some reason this does not work, -- not counting layers #self.last_file_pos= f.tell() #print('Saved position: {}'.format(self.last_file_pos)) break self.current_z = z found_layer = True Logger.debug( "GcodeViewerScreen: x= {}, y= {}, z= {}, s= {}".format( x, y, z, s)) # find bounding box if math.isnan(min_x) or x < min_x: min_x = x if math.isnan(min_y) or y < min_y: min_y = y if math.isnan(max_x) or x > max_x: max_x = x if math.isnan(max_y) or y > max_y: max_y = y # accumulating vertices is more efficient but we need to flush them at some point # Here we flush them if we encounter a new G code like G3 following G1 if last_gcode != gcode: # flush vertices if points: self.canv.add(Color(0, 0, 0)) self.canv.add( Line(points=points, width=1, cap='none', joint='none')) points = [] last_gcode = gcode # in slicer generated files there is no G0 so we need a way to know when to draw, so if there is an E then draw else don't if gcode == 0: #print("move to: {}, {}, {}".format(x, y, z)) # draw moves in dashed red self.canv.add(Color(1, 0, 0)) self.canv.add( Line(points=[lastpos[0], lastpos[1], x, y], width=1, dash_offset=1, cap='none', joint='none')) elif gcode == 1: if ('X' in d or 'Y' in d): if self.laser_mode and s <= 0.01: # do not draw non cutting lines if points: # draw accumulated points upto this point self.canv.add(Color(0, 0, 0)) self.canv.add( Line(points=points, width=1, cap='none', joint='none')) points = [] # for 3d printers (has_e) only draw if there is an E elif not has_e or 'E' in d: # if a CNC gcode file or there is an E in the G1 (3d printing) #print("draw to: {}, {}, {}".format(x, y, z)) # collect points but don't draw them yet if len(points) < 2: points.append(lastpos[0]) points.append(lastpos[1]) points.append(x) points.append(y) else: # a G1 with no E, treat as G0 and draw moves in red #print("move to: {}, {}, {}".format(x, y, z)) if points: # draw accumulated points upto this point self.canv.add(Color(0, 0, 0)) self.canv.add( Line(points=points, width=1, cap='none', joint='none')) points = [] # now draw the move in red self.canv.add(Color(1, 0, 0)) self.canv.add( Line(points=[lastpos[0], lastpos[1], x, y], width=1, cap='none', joint='none')) else: # A G1 with no X or Y, maybe E only move (retract) or Z move (layer change) if points: # draw accumulated points upto this point self.canv.add(Color(0, 0, 0)) self.canv.add( Line(points=points, width=1, cap='none', joint='none')) points = [] elif gcode in [2, 3]: # CW=2,CCW=3 circle # code cribbed from bCNC xyz = [] xyz.append((lastpos[0], lastpos[1], lastpos[2])) uc, vc = self.motionCenter(gcode, plane, lastpos, [x, y, z], i, j) if plane == XY: u0 = lastpos[0] v0 = lastpos[1] w0 = lastpos[2] u1 = x v1 = y w1 = z elif plane == XZ: u0 = lastpos[0] v0 = lastpos[2] w0 = lastpos[1] u1 = x v1 = z w1 = y gcode = 5 - gcode # flip 2-3 when XZ plane is used else: u0 = lastpos[1] v0 = lastpos[2] w0 = lastpos[0] u1 = y v1 = z w1 = x phi0 = math.atan2(v0 - vc, u0 - uc) phi1 = math.atan2(v1 - vc, u1 - uc) try: sagitta = 1.0 - CNC_accuracy / self.rval except ZeroDivisionError: sagitta = 0.0 if sagitta > 0.0: df = 2.0 * math.acos(sagitta) df = min(df, math.pi / 4.0) else: df = math.pi / 4.0 if gcode == 2: if phi1 >= phi0 - 1e-10: phi1 -= 2.0 * math.pi ws = (w1 - w0) / (phi1 - phi0) phi = phi0 - df while phi > phi1: u = uc + self.rval * math.cos(phi) v = vc + self.rval * math.sin(phi) w = w0 + (phi - phi0) * ws phi -= df if plane == XY: xyz.append((u, v, w)) elif plane == XZ: xyz.append((u, w, v)) else: xyz.append((w, u, v)) else: if phi1 <= phi0 + 1e-10: phi1 += 2.0 * math.pi ws = (w1 - w0) / (phi1 - phi0) phi = phi0 + df while phi < phi1: u = uc + self.rval * math.cos(phi) v = vc + self.rval * math.sin(phi) w = w0 + (phi - phi0) * ws phi += df if plane == XY: xyz.append((u, v, w)) elif plane == XZ: xyz.append((u, w, v)) else: xyz.append((w, u, v)) xyz.append((x, y, z)) # plot the points points = [] for t in xyz: x1, y1, z1 = t points.append(x1) points.append(y1) max_x = max(x1, max_x) min_x = min(x1, min_x) max_y = max(y1, max_y) min_y = min(y1, min_y) self.canv.add(Color(0, 0, 0)) self.canv.add( Line(points=points, width=1, cap='none', joint='none')) points = [] # always remember last position lastpos = [x, y, z] laste = e lasts = s if not found_layer: # we hit the end of file before finding the layer we want Logger.info( "GcodeViewerScreen: last layer was at {}".format(lastz)) self.last_target_layer -= 1 return # flush any points not yet drawn if points: # draw accumulated points upto this point self.canv.add(Color(0, 0, 0)) self.canv.add( Line(points=points, width=1, cap='none', joint='none')) points = [] # center the drawing and scale it dx = max_x - min_x dy = max_y - min_y if dx == 0 or dy == 0: Logger.warning( "GcodeViewerScreen: size is bad, maybe need 2D mode") return dx += 4 dy += 4 Logger.debug("GcodeViewerScreen: dx= {}, dy= {}".format(dx, dy)) # add in the translation to center object self.tx = -min_x - dx / 2 self.ty = -min_y - dy / 2 self.canv.insert(1, Translate(self.tx, self.ty)) Logger.debug("GcodeViewerScreen: tx= {}, ty= {}".format( self.tx, self.ty)) # scale the drawing to fit the screen if abs(dx) > abs(dy): scale = self.ids.surface.width / abs(dx) if abs(dy) * scale > self.ids.surface.height: scale *= self.ids.surface.height / (abs(dy) * scale) else: scale = self.ids.surface.height / abs(dy) if abs(dx) * scale > self.ids.surface.width: scale *= self.ids.surface.width / (abs(dx) * scale) Logger.debug("GcodeViewerScreen: scale= {}".format(scale)) self.scale = scale self.canv.insert(1, Scale(scale)) # translate to center of canvas self.offs = self.ids.surface.center self.canv.insert( 1, Translate(self.ids.surface.center[0], self.ids.surface.center[1])) Logger.debug("GcodeViewerScreen: cx= {}, cy= {}".format( self.ids.surface.center[0], self.ids.surface.center[1])) Logger.debug("GcodeViewerScreen: sx= {}, sy= {}".format( self.ids.surface.size[0], self.ids.surface.size[1])) # axis Markers self.canv.add(Color(0, 1, 0, mode='rgb')) self.canv.add( Line(points=[0, -10, 0, self.ids.surface.height / scale], width=1, cap='none', joint='none')) self.canv.add( Line(points=[-10, 0, self.ids.surface.width / scale, 0], width=1, cap='none', joint='none')) # tool position marker if self.app.is_connected: x = self.app.wpos[0] y = self.app.wpos[1] r = (10.0 / self.ids.surface.scale) / scale self.canv.add(Color(1, 0, 0, mode='rgb', group="tool")) self.canv.add(Line(circle=(x, y, r), group="tool")) # self.canv.add(Rectangle(pos=(x, y-r/2), size=(1/scale, r), group="tool")) # self.canv.add(Rectangle(pos=(x-r/2, y), size=(r, 1/scale), group="tool")) self.canv.add(PopMatrix()) self._loaded_ok = True Logger.debug("GcodeViewerScreen: done loading") def update_tool(self, i, v): if not self.is_visible or not self.app.is_connected: return # follow the tool path #self.canv.remove_group("tool") x = v[0] y = v[1] r = (10.0 / self.ids.surface.scale) / self.scale g = self.canv.get_group("tool") if g: g[2].circle = (x, y, r) # g[4].pos= x, y-r/2 # g[6].pos= x-r/2, y def transform_to_wpos(self, posx, posy): ''' convert touch coords to local scatter widget coords, relative to lower bottom corner ''' pos = self.ids.surface.to_widget(posx, posy) # convert to original model coordinates (mm), need to take into account scale and translate wpos = ((pos[0] - self.offs[0]) / self.scale - self.tx, (pos[1] - self.offs[1]) / self.scale - self.ty) return wpos def transform_to_spos(self, posx, posy): ''' inverse transform of model coordinates to scatter coordinates ''' pos = ((((posx + self.tx) * self.scale) + self.offs[0]), (((posy + self.ty) * self.scale) + self.offs[1])) spos = self.ids.surface.to_window(*pos) #print("pos= {}, spos= {}".format(pos, spos)) return spos def moved(self, w, touch): # we scaled or moved the scatter so need to reposition cursor # TODO it would be nice if the cursor stayed where it was relative to the model during a move or scale # NOTE right now we can't move or scale while cursor is on # if self.select_mode: # x, y= (self.crossx[0].pos[0], self.crossx[1].pos[1]) # self.stop_cursor(x, y) # self.start_cursor(x, y) # hide tool marker self.canv.remove_group('tool') def start_cursor(self, x, y): tx, ty = self.transform_to_wpos(x, y) label = CoreLabel(text="{:1.2f},{:1.2f}".format(tx, ty)) label.refresh() texture = label.texture px, py = (x, y) with self.ids.surface.canvas.after: Color(0, 0, 1, mode='rgb', group='cursor_group') self.crossx = [ Rectangle(pos=(px, 0), size=(1, self.height), group='cursor_group'), Rectangle(pos=(0, py), size=(self.width, 1), group='cursor_group'), Line(circle=(px, py, 20), group='cursor_group'), Rectangle(texture=texture, pos=(px - texture.size[0] / 2, py - 40), size=texture.size, group='cursor_group') ] def move_cursor_by(self, dx, dy): x, y = (self.crossx[0].pos[0] + dx, self.crossx[1].pos[1] + dy) self.crossx[0].pos = x, 0 self.crossx[1].pos = 0, y self.crossx[2].circle = (x, y, 20) tx, ty = self.transform_to_wpos(x, y) label = CoreLabel(text="{:1.2f},{:1.2f}".format(tx, ty)) label.refresh() texture = label.texture self.crossx[3].texture = texture self.crossx[3].pos = x - texture.size[0] / 2, y - 40 def stop_cursor(self, x=0, y=0): self.ids.surface.canvas.after.remove_group('cursor_group') self.crossx = None def on_touch_down(self, touch): #print(self.ids.surface.bbox) if self.ids.view_window.collide_point(touch.x, touch.y): # if within the scatter window if self.select_mode: touch.grab(self) return True elif touch.is_mouse_scrolling: # Allow mouse scroll wheel to zoom in/out if touch.button == 'scrolldown': # zoom in if self.ids.surface.scale < 100: rescale = 1.1 self.ids.surface.apply_transform( Matrix().scale(rescale, rescale, rescale), post_multiply=True, anchor=self.ids.surface.to_widget(*touch.pos)) elif touch.button == 'scrollup': # zoom out if self.ids.surface.scale > 0.01: rescale = 0.8 self.ids.surface.apply_transform( Matrix().scale(rescale, rescale, rescale), post_multiply=True, anchor=self.ids.surface.to_widget(*touch.pos)) self.moved(None, touch) return True return super(GcodeViewerScreen, self).on_touch_down(touch) def on_touch_move(self, touch): if self.select_mode: if touch.grab_current is not self: return False dx = touch.dpos[0] dy = touch.dpos[1] self.move_cursor_by(dx, dy) return True else: return super(GcodeViewerScreen, self).on_touch_move(touch) def on_touch_up(self, touch): if touch.grab_current is self: touch.ungrab(self) return True return super(GcodeViewerScreen, self).on_touch_up(touch) def select(self, on): if not on and self.select_mode: self.stop_cursor() self.select_mode = False elif on and not self.select_mode: x, y = self.center self.start_cursor(x, y) self.select_mode = True def move_gantry(self): if not self.select_mode: return self.select_mode = False self.ids.select_mode_but.state = 'normal' # convert to original model coordinates (mm), need to take into account scale and translate x, y = (self.crossx[0].pos[0], self.crossx[1].pos[1]) self.stop_cursor(x, y) wpos = self.transform_to_wpos(x, y) if self.comms: self.comms.write('G0 X{:1.2f} Y{:1.2f}\n'.format(wpos[0], wpos[1])) else: print('Move Gantry to: {:1.2f}, {:1.2f}'.format(wpos[0], wpos[1])) print('G0 X{:1.2f} Y{:1.2f}'.format(wpos[0], wpos[1])) def set_wcs(self): if not self.select_mode: return self.select_mode = False self.ids.select_mode_but.state = 'normal' # convert to original model coordinates (mm), need to take into account scale and translate x, y = (self.crossx[0].pos[0], self.crossx[1].pos[1]) self.stop_cursor(x, y) wpos = self.transform_to_wpos(x, y) if self.comms: self.comms.write('G10 L20 P0 X{:1.2f} Y{:1.2f}\n'.format( wpos[0], wpos[1])) else: print('Set WCS to: {:1.2f}, {:1.2f}'.format(wpos[0], wpos[1])) print('G10 L20 P0 X{:1.2f} Y{:1.2f}'.format(wpos[0], wpos[1])) def set_type(self, t): if t == '3D': self.twod_mode = False self.laser_mode = False elif t == '2D': self.twod_mode = True self.laser_mode = False elif t == 'Laser': self.twod_mode = True self.laser_mode = True self.loading(0 if self.twod_mode else 1)
class MapViewer(ScatterPlane): def __init__(self, **kwargs): kwargs.setdefault('do_rotation', False) kwargs.setdefault('show_border', False) kwargs.setdefault('close_on_idle', False) kwargs.setdefault('scale_min', 1) super(MapViewer, self).__init__(**kwargs) # init ScatterPlane with above parameters self.map = None self.tilesize = (0, 0) self.tileCache = TileCache() self._zoom = 0 # intern var managed by a property self.reticule = None self.show_arrow = False self.arrow = type('DefaultArrow', (), {'azimuth': 0}) # creation a a default object, so azimth can still be set self.idle = True # used by self.update # variable contenant la dernière position gps self.last_pos = (0, 0) self.last_pos_wgs84 = (0, 0) self.locked_on_pos = False # The path self.path = None self.path_width = 5.0 self.tracking_path = False self._path_zoom = 0 # intern var to track a zoom change # Layers self.map_layer = None self.path_layer = None # Variables relative to layers self.map_cleanup_scheduled = False # Reposition tasks self.reposition_executor = None # Finally, as every user action implies a change of x and/or y value, # we bind those properties change on the update self.bind(x=self.update, y=self.update) def view_map(self, _map): # Prepare the map self._prepare_map(_map) # Set the last pos in map coord if possible self._init_last_pos(_map) # Adding the map layer self.map_layer = InstructionGroup() self.canvas.add(self.map_layer) # Adding the path layer self.path_layer = InstructionGroup() self.path_layer.add(Color(1, 0, 0, 0.5)) self.path = Line(width=self.path_width) self.path_layer.add(self.path) self.canvas.add(self.path_layer) # Creation of the reposition task executor # Update the reticule and if needed the arrow position self.reposition_executor = RepositionExecutor(0.1) if self.reticule is not None: self.reposition_executor.add_reposition_task(lambda: self.set_reticule_pos(*self.last_pos)) self.reposition_executor.add_reposition_task(lambda: self.set_arrow_pos()) # The first time we view a map we have to trigger the first tile drawing and reposition the reticule Clock.schedule_once(self.update_map, 0) self.reposition_executor.execute() def view_map_for_calibration(self, _map): # Prepare the map self._prepare_map(_map) # Adding the map layer self.map_layer = InstructionGroup() self.canvas.add(self.map_layer) # Creation of the reposition task executor self.reposition_executor = RepositionExecutor(0.1) # The first time we view a map we have to trigger the first tile drawing and reposition the reticule Clock.schedule_once(self.update_map, 0) def _prepare_map(self, _map): # Cleanup the canvas, reinitialize variables self.canvas.clear() self.idle = True self.scale = 1 self.pos = 0, 0 self.tileCache.__init__() self.map = _map self.tilesize = _map.get_tile_size() # Here we set the maximum scale of the Scatter, information retrieved from max_zoom in calibration.ini self.scale_max = pow(2, _map.get_max_zoom()) # Save the path of the map's calibration file in application settings config.save_last_map_path(_map.calibration_file_path) def update(self, obj, value, delay=UPDATE_DELAY): """ To prevent overuse of update_map, each change of position triggers an update of the map after UPDATE_DELAY sec. For instance, the position of the reticule has to be updated. """ if self.map is None: return # Pause the Loader, to prevent ui blocking Loader.pause() # Reposition widgets that need to be repositioned self.reposition_executor.execute() # Trigger the map update if self.idle: self.idle = False else: Clock.unschedule(self.update_map) Clock.schedule_once(self.update_map, delay) # Resume the Loader after a short time Clock.schedule_once(MapViewer.resume_loading, 0.1) def update_map(self, dt): # First, we get the current area covered on the ScatterPlane area = self.get_covered_area() map_area = (0, 0, self.tilesize[0], self.tilesize[1]) # Then, we make the list of tiles corresponding to that area tile_list = self.generate_tile_list(self.zoom, map_area, area) # Before drawing the tiles, we may have to perform a cleanup of the map layer if self.map_cleanup_scheduled: self.map_layer.clear() self.tileCache.__init__() self.map_cleanup_scheduled = False # Tiles drawing self.draw_tiles(tile_list) # We schedule a cleanup, which will be done if the app is inactive at the time of the callback execution Clock.schedule_once(partial(self.cleanup, tile_list), CLEANUP_DELAY) self.idle = True # logger.debug("container : %s" % self.tileCache.container.values()) # If we are showing the path, we update its view (adjust the thickness) if self.tracking_path: self.format_path() @staticmethod def resume_loading(dt): Loader.resume() @property def zoom(self): """Get zoom from current scale""" # At each zoom step forward, we cover an area twice as small as the former area self._zoom = log(self.scale, 2) return self._zoom def get_covered_area(self): parent = self.parent xmin = parent.x xmax = parent.x + parent.width ymin = parent.y ymax = parent.y + parent.height # Coordinates in ScatterPlane # Here, local and window coordinates are the same because ScatterPlane's origin # corresponds to the windows's origin xmin, ymin = self.to_local(xmin, ymin) # (x,y) coord of the bottom left corner xmax, ymax = self.to_local(xmax, ymax) # (x,y) coord of the top right corner return xmin, ymin, xmax, ymax def out_of_scope(self): logger.debug("OUT OF SCOPE") return [] @staticmethod def extend_tile_view(tab, maxindex): if len(tab): if tab[0] > 0: tab.insert(0, tab[0] - 1) if tab[-1] < maxindex - 1: tab.append(tab[-1] + 1) def generate_tile_list(self, zoom, map_area, area): """ Generates the list of tiles that should be visible for the given zoom level and the area visible on the scatterPlane """ xmin0, ymin0, xmax0, ymax0 = map_area # area for zoom=0, one unique tile xmin, ymin, xmax, ymax = area # Overlap test if xmax <= xmin0 or xmin >= xmax0 or ymin >= ymax0 or ymax <= ymin0: return self.out_of_scope() # coordinates of the intersection between map_area and area xmin_inter = max(xmin0, xmin) xmax_inter = min(xmax0, xmax) ymin_inter = max(ymin0, ymin) ymax_inter = min(ymax0, ymax) # If the current zoom is already an integer, we take its value. # Otherwise, we take its superior int value because we want to # identify which tiles of the next zoom level have to be displayed zoom_int = int(zoom if zoom == int(zoom) else int(zoom) + 1) targeted_scale = pow(2, zoom_int) tile_width = (xmax0 - xmin0) / float(targeted_scale) startx_index = int(xmin_inter / tile_width) endx_index = int(xmax_inter / tile_width) # Calculation of the indexes on x axis x_indexes = [] append = x_indexes.append for x in range(startx_index, endx_index + 1): append(x) MapViewer.extend_tile_view(x_indexes, targeted_scale) starty_index = int(ymin_inter / tile_width) endy_index = int(ymax_inter / tile_width) # Calculation of the indexes on y axis y_indexes = [] append = y_indexes.append for y in range(starty_index, endy_index + 1): append(y) MapViewer.extend_tile_view(y_indexes, targeted_scale) tile_list = [] append = tile_list.append for x in x_indexes: for y in y_indexes: tile = Tile() # tile.canvas = self.canvas tile.pos = Vector(x, y) * tile_width tile.size = (tile_width, tile_width) tile.x = x tile.y = y tile.zoom = zoom_int append(tile) return tile_list # The good damn right way to do it def draw_tile(self, proxy): if proxy.image.texture: self.map_layer.add( Rectangle(pos=proxy.pos, size=proxy.size, texture=proxy.image.texture, group=proxy.zoom)) def draw_tiles(self, tile_list): for tile in tile_list: image_id = tile.get_id() if self.tileCache.add_tile(tile.zoom, image_id): image = self.map.get_tile(tile.zoom, tile.x, tile.y) if image is None: continue image.create_property("pos", tile.pos) image.create_property("size", tile.size) image.create_property("zoom", str(int(tile.zoom))) image.bind(on_load=self.draw_tile) # if image.loaded: # only useful when Loader actually caches images # image.dispatch("on_load") def cleanup(self, tile_list, *largs): """ Cleanup is achieved when the app is considered inactive, ie when self.idle = True. """ if not self.idle: return zoom = self.zoom zoom_int = int(zoom if zoom == int(zoom) else int(zoom) + 1) print "debut cleanup, conserve zoom", zoom_int for _zoom in TileCache.get_unnecessary_zooms(self.tileCache.container.keys(), zoom_int): try: print "suppr zoom", _zoom self.map_layer.remove_group(str(_zoom)) self.tileCache.remove_tiles_for_zoom(_zoom) except: logger.debug("the canvas doesn't contains the zoom %s" % _zoom) if self.tileCache.is_tile_overfull(zoom_int): self.map_cleanup_scheduled = True # logger.debug("cleanup done, container : %s" % self.tileCache.container.values()) def set_reticule(self, reticule): self.reticule = reticule def set_reticule_pos(self, x, y): """ :param x: x position on the map in the range [0,tile_width] :param y: y position on the map in the range [0,tile_height] :return: """ # TODO : remplacer cette méthode par set_movable_widget_pos ? (cf plus bas) self.reticule.set_center_x(self.x + x * self.scale) self.reticule.set_center_y(self.y + y * self.scale) def set_movable_widget_pos(self, widget, x, y): """ Position a widget given the local coordinates of its center. :param x: x position on the map in the range [0,tile_width] :param y: y position on the map in the range [0,tile_height] """ if widget: widget.set_center_x(self.x + x * self.scale) widget.set_center_y(self.y + y * self.scale) def add_movable_widget(self, widget): """Actions done when a movable widget is added to the MapViewer.""" # Add a reposition task self.reposition_executor.add_reposition_task(lambda: self.set_movable_widget_pos( widget, *widget.pos_local)) def set_arrow_pos(self): """Must be called after set_reticule_pos""" if self.show_arrow: self.arrow.set_center_x(self.reticule.get_center_x()) self.arrow.set_center_y(self.reticule.get_center_y()) def set_orientation_arrow(self, arrow): self.show_arrow = True self.arrow = arrow def update_azimuth(self, instance, angle): self.arrow.azimuth = -angle def _init_last_pos(self, _map): """Initialize the last position in the _map projection coordinates, given the last known wgs84 position. This is used in view_map method. """ if self.last_pos_wgs84 is not None: self.last_pos = _map.get_map_coord(*self.last_pos_wgs84) else: self.last_pos = (0, 0) def update_pos(self, instance, value): # Remember the position, even if there is no map self.last_pos_wgs84 = value # If there is no map, no need to go further if self.map is None: return # Conversion from wgs84 coord to map coord x, y = self.map.get_map_coord(*value) # Remember this position too self.last_pos = x, y # Update the reticule pos self.set_reticule_pos(x, y) # Update the orientation arrow pos if necessary if self.show_arrow: self.set_arrow_pos() # Update the path if self.tracking_path: self.update_path(x, y) # If we are locked on pos, we center the view on the last known position if self.locked_on_pos: self.center_on_last_pos() def update_path(self, x, y): self.path.points += x, y def toggle_tracking_path(self, obj): if self.tracking_path: self.tracking_path = False # Creation of a new path self.path = Line(width=self.path_width) else: self.tracking_path = True def format_path(self, *args): """This updates the width of the path to be consistent with the scale. """ if self._path_zoom != self.zoom: self.path.width = self.path_width / self.scale self._path_zoom = self.zoom def center_on_last_pos(self, *args): scale = self.scale new_x = Window.size[0]/2 - self.last_pos[0]*scale new_y = Window.size[1]/2 - self.last_pos[1]*scale Animation.cancel_all(self) anim = Animation(x=new_x, y=new_y, t='in_out_quad', duration=0.5) anim.start(self) def get_dist_to_center(self): """ :return: The distance in meters between the last known position and the position represented by the middle of the screen. If no distance can be calculated, it returns -1. """ if self.map is None: return -1 try: merc_x, merc_y = self.map.map_coord_to_map_projection(*self.to_local(Window.size[0]/2, Window.size[1]/2)) except ZeroDivisionError: return -1 lat, lon = Map.to_geographic(merc_x, merc_y) lat_last, lon_last = self.last_pos_wgs84 dist = Map.distance(lat_last, lon_last, lat, lon) return dist def transform_with_touch(self, touch): if self.locked_on_pos: if len(self._touches) == 1: return False changed = False # We have more than one touch... list of last known pos points = [Vector(self._last_touch_pos[t]) for t in self._touches if t is not touch] # Add current touch last points.append(Vector(touch.pos)) # We only want to transform if the touch is part of the two touches # farthest apart! So first we find anchor, the point to transform # around as another touch farthest away from current touch's pos anchor_ = max(points[:-1], key=lambda p: p.distance(touch.pos)) # Now we find the touch farthest away from anchor, if its not the # same as touch. Touch is not one of the two touches used to transform farthest = max(points, key=anchor_.distance) if farthest is not points[-1]: return changed # Ok, so we have touch, and anchor, so we can actually compute the # transformation old_line = Vector(*touch.ppos) - anchor_ new_line = Vector(*touch.pos) - anchor_ if not old_line.length(): # div by zero return changed # pol : we don't want rotation here # angle = radians(new_line.angle(old_line)) * self.do_rotation # self.apply_transform(Matrix().rotate(angle, 0, 0, 1), anchor=anchor) # pol : trick -> change the origin!! anchor = Vector(self.to_parent(*self.last_pos)) if self.do_scale: scale = new_line.length() / old_line.length() new_scale = scale * self.scale if new_scale < self.scale_min: scale = self.scale_min / self.scale elif new_scale > self.scale_max: scale = self.scale_max / self.scale self.apply_transform(Matrix().scale(scale, scale, scale), anchor=anchor) changed = True return changed super(MapViewer, self).transform_with_touch(touch)