Ejemplo n.º 1
0
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)
Ejemplo n.º 2
0
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)