diff --git a/cc2me/savedata/constants.py b/cc2me/savedata/constants.py index e05a636..b709ab3 100644 --- a/cc2me/savedata/constants.py +++ b/cc2me/savedata/constants.py @@ -1,7 +1,10 @@ +import os import random from abc import ABC from enum import Enum -from typing import cast, List, Optional, Union +from typing import cast, List, Optional, Union, Dict +from xml.etree import ElementTree +from xml.etree.ElementTree import Element MAX_INTEGER = 4294967295 @@ -421,10 +424,14 @@ def __init__(self, count: int, *vtype: VehicleType): AmmunitionCapacity(a_type, 1, VehicleType.Manta, VehicleType.Petrel, VehicleType.Razorbill, VehicleType.Albatross)) -ATTACHMENT_CAPACITY.append( +ATTACHMENT_CAPACITY.extend([ AmmunitionCapacity(VehicleAttachmentDefinitionIndex.Gun20mm, + 500, VehicleType.Mule + ), + AmmunitionCapacity(VehicleAttachmentDefinitionIndex.Gun40mm, 500, VehicleType.Mule ) + ] ) ATTACHMENT_CAPACITY.append( AmmunitionCapacity(VehicleAttachmentDefinitionIndex.Refuel, @@ -441,6 +448,11 @@ def __init__(self, count: int, *vtype: VehicleType): 400, VehicleType.Mule ) ) +ATTACHMENT_CAPACITY.append( + AmmunitionCapacity(VehicleAttachmentDefinitionIndex.Rearm40mm, + 300, VehicleType.Mule + ) +) ATTACHMENT_CAPACITY.extend( [ AmmunitionCapacity(VehicleAttachmentDefinitionIndex.BattleDroids, @@ -508,3 +520,33 @@ def get_default_state(v_type: VehicleType) -> List[Capacity]: if v_type in item.vtypes: values.append(item) return values + + +def get_cc2_appdata() -> str: + cc2dir = os.path.expandvars(r'%APPDATA%\\Carrier Command 2') + return cc2dir + + +def get_persistent_file_path() -> str: + persistent = os.path.join(get_cc2_appdata(), "persistent_data.xml") + return persistent + + +def read_save_slots(slots_file: Optional[str] = None) -> List[Dict[str, str]]: + if slots_file is None: + slots_file = get_persistent_file_path() + slots = [] + with open(slots_file, "r") as xmlfile: + xml = xmlfile.read() + etree = ElementTree.fromstring(xml) + for item in etree: + if isinstance(item, Element): + filename = item.attrib.get("save_name") + text = item.attrib.get("display_name") + if filename: + slots.append({ + "filename": filename, + "display": text + }) + return slots + diff --git a/cc2me/savedata/loader.py b/cc2me/savedata/loader.py index f3a6038..d04070b 100644 --- a/cc2me/savedata/loader.py +++ b/cc2me/savedata/loader.py @@ -1,4 +1,4 @@ -from typing import Optional, List +from typing import Optional, List, Tuple from xml.etree.ElementTree import Element import os import re @@ -8,9 +8,9 @@ from .types.abstract import CC2Save from .types.teams import Team from .types.tiles import Tile +from .types.spawndata import VehicleSpawnData, VehicleSpawn from .types.vehicles.vehicle import Vehicle from .types.vehicles.vehicle_state import VehicleStateContainer -from ..paths import SCHEMA from .logging import logger from .constants import POS_Y_SEABOTTOM, BIOME_SANDY_PINES, VehicleType, get_default_state @@ -125,6 +125,20 @@ def tile(self, tile_id: int) -> Tile: raise KeyError(tile_id) + def spawn(self, spawn_id: int) -> Optional[VehicleSpawn]: + for tile in self.tiles: + for spawn in tile.spawn_data.vehicles.items(): + if spawn.data.respawn_id == spawn_id: + return spawn + return None + + def get_spawn_tile(self, spawn_id: int) -> Tuple[Optional[Tile], Optional[VehicleSpawn]]: + for tile in self.tiles: + for spawn in tile.spawn_data.vehicles.items(): + if spawn.data.respawn_id == spawn_id: + return tile, spawn + return None, None + def find_vehicles_by_definition(self, definition_id: int): return [ x for x in self.vehicles if x.definition_index == definition_id @@ -181,6 +195,17 @@ def remove_vehicle(self, vehicle: Vehicle): vsparent = self.vehicle_states_parent if vehicle: + if vehicle.definition_index == VehicleType.Jetty.value: + return # dont' delete a jetty, it causes problems if the carrier isnt launched + if vehicle.definition_index == VehicleType.Carrier.value: + # if this is the last carrier a team has, mark the team as destroyed. + team_carriers = [x for x in self.vehicles + if x.team_id == vehicle.team_id and x.definition_index == vehicle.definition_index] + if len(team_carriers) == 1: + # destroy the team + team = self.team(vehicle.team_id) + team.is_destroyed = True + vid = str(vehicle.id) state = vehicle.state if state: @@ -190,6 +215,15 @@ def remove_vehicle(self, vehicle: Vehicle): ve = [x for x in vparent if x.attrib.get("id") == vid] if ve: vparent.remove(ve[0]) + # if there was a spawn, remove that too + spawn = self.spawn(vehicle.id) + if spawn: + self.remove_spawn(vehicle.id) + + def remove_spawn(self, spawn_id: int): + tile, spawn = self.get_spawn_tile(spawn_id) + if spawn and tile: + tile.spawn_data.vehicles.remove(spawn_id) @property def _teams(self) -> List[Element]: diff --git a/cc2me/savedata/types/spawndata.py b/cc2me/savedata/types/spawndata.py index f27e8bd..b8d646b 100644 --- a/cc2me/savedata/types/spawndata.py +++ b/cc2me/savedata/types/spawndata.py @@ -1,4 +1,4 @@ -from typing import cast, Iterable, Optional, List, Any +from typing import cast, Iterable, Optional, List, Any, Union from xml.etree.ElementTree import Element from .utils import ElementProxy, IsSetMixin, e_property, IntAttribute, FloatAttribute, WorldPosition, \ @@ -70,6 +70,11 @@ def move(self, x: float, y: float, z: float) -> None: self.data.world_position.y = y self.data.world_position.z = z + def translate(self, dx: float, dy: float, dz: float) -> None: + self.move(self.data.world_position.x + dx, + self.data.world_position.y + dy, + self.data.world_position.z + dz) + @property def loc(self) -> Location: pos = self.data.world_position @@ -99,11 +104,16 @@ def items(self) -> Iterable[VehicleSpawn]: ret.append(VehicleSpawn(element, self.cc2obj)) return ret - def remove(self, child: VehicleSpawn): + def remove(self, child: Union[VehicleSpawn, int]): + remove_ident = -1 + if isinstance(child, VehicleSpawn): + remove_ident = child.data.respawn_id + elif isinstance(child, int): + remove_ident = child children = self.items() for item in children: item: VehicleSpawn - if item.data.respawn_id == child.data.respawn_id: + if item.data.respawn_id == remove_ident: self.element.remove(item.element) def append(self, item: VehicleSpawn): @@ -115,7 +125,7 @@ class SpawnData(ElementProxy, IsSetMixin): team_id = e_property(IntAttribute("team_id", default_value=0)) def defaults(self): - self.is_set = False + self.is_set = True @property def vehicles(self) -> VehicleSpawnContainer: diff --git a/cc2me/savedata/types/tiles.py b/cc2me/savedata/types/tiles.py index 2a71c33..292e4ec 100644 --- a/cc2me/savedata/types/tiles.py +++ b/cc2me/savedata/types/tiles.py @@ -118,6 +118,11 @@ def set_position(self, *, def move(self, x: float, y: float, z: float) -> None: self.set_position(x=x, y=y, z=z) + def translate(self, dx: float, dy: float, dz: float) -> None: + self.set_position(x=self.loc.x + dx, + y=self.loc.y + dy, + z=self.loc.z + dz) + @property def loc(self) -> Location: return Location(self.world_position.x, self.world_position.y, self.world_position.z) diff --git a/cc2me/savedata/types/utils.py b/cc2me/savedata/types/utils.py index 93aad7a..4045023 100644 --- a/cc2me/savedata/types/utils.py +++ b/cc2me/savedata/types/utils.py @@ -190,6 +190,10 @@ class MovableLocationMixin(LocationMixin, ABC): def move(self, x: float, y: float, z: float) -> None: pass + @abstractmethod + def translate(self, dx: float, dy: float, dz: float) -> None: + pass + class WorldPosition(Point3D): tag = "world_position" diff --git a/cc2me/savedata/types/vehicles/vehicle.py b/cc2me/savedata/types/vehicles/vehicle.py index 3849c7c..c7bd8ec 100644 --- a/cc2me/savedata/types/vehicles/vehicle.py +++ b/cc2me/savedata/types/vehicles/vehicle.py @@ -110,6 +110,11 @@ def set_location(self, def move(self, x: float, y: float, z: float) -> None: self.set_location(x, y, z) + def translate(self, dx: float, dy: float, dz: float) -> None: + self.set_location(self.loc.x + dx, + self.loc.y + dy, + self.loc.z + dz) + @property def loc(self) -> Location: return Location(self.transform.tx, self.transform.ty, self.transform.tz) diff --git a/cc2me/ui/icons/dupe.png b/cc2me/ui/icons/dupe.png new file mode 100644 index 0000000..730c1d2 Binary files /dev/null and b/cc2me/ui/icons/dupe.png differ diff --git a/cc2me/ui/icons/dupe.xcf b/cc2me/ui/icons/dupe.xcf new file mode 100644 index 0000000..995655b Binary files /dev/null and b/cc2me/ui/icons/dupe.xcf differ diff --git a/cc2me/ui/saveslotchooser.py b/cc2me/ui/saveslotchooser.py index dc2ffb9..d11aa0e 100644 --- a/cc2me/ui/saveslotchooser.py +++ b/cc2me/ui/saveslotchooser.py @@ -5,6 +5,8 @@ from xml.etree.ElementTree import Element from tkinter import simpledialog +from cc2me.savedata.constants import read_save_slots + class SlotChooser(simpledialog.Dialog): @@ -22,22 +24,10 @@ def body(self, master) -> None: command=callback) btn.pack(fill=tkinter.X) - def __init__(self, app, persistent_file: str, choice: dict, title="Load a save slot", ): + def __init__(self, app, persistent_file: str, choice: dict, title="Load a save slot"): self.persistent_file = persistent_file - self.slots = [] + self.slots = read_save_slots(persistent_file) self.choice = choice - with open(self.persistent_file, "r") as xmlfile: - xml = xmlfile.read() - self.etree = ElementTree.fromstring(xml) - for item in self.etree: - if isinstance(item, Element): - filename = item.attrib.get("save_name") - text = item.attrib.get("display_name") - if filename: - self.slots.append({ - "filename": filename, - "display": text - }) super(SlotChooser, self).__init__(parent=app, title=title) diff --git a/cc2me/ui/tool.py b/cc2me/ui/tool.py index d392b81..3318d33 100644 --- a/cc2me/ui/tool.py +++ b/cc2me/ui/tool.py @@ -8,7 +8,7 @@ from typing import Optional, List from .properties import Properties -from ..savedata.constants import get_island_name, VehicleType, VehicleAttachmentDefinitionIndex +from ..savedata.constants import VehicleType, VehicleAttachmentDefinitionIndex, get_persistent_file_path, get_cc2_appdata from ..savedata.types.objects import Island, Unit, get_unit, Spawn from ..savedata.types.tiles import Tile from ..savedata.loader import CC2XMLSave, load_save_file @@ -25,8 +25,8 @@ class App(tkinter.Tk): WIDTH = 900 HEIGHT = 750 - cc2dir = os.path.expandvars(r'%APPDATA%\\Carrier Command 2') - persistent = os.path.join(cc2dir, "persistent_data.xml") + cc2dir = get_cc2_appdata() + persistent = get_persistent_file_path() def __init__(self, *args, **kwargs): tkinter.Tk.__init__(self, *args, **kwargs) @@ -239,7 +239,6 @@ def duplicate_selected(self): if new_units: self.select_markers(new_units) - def add_new_seal(self): self.add_new_unit(VehicleType.Seal) @@ -272,7 +271,11 @@ def remove_item(self): self.cc2me.remove_tile(tile) if isinstance(marker, UnitMarker): vehicle = marker.unit.vehicle() - self.cc2me.remove_vehicle(vehicle) + if vehicle is not None: + self.cc2me.remove_vehicle(vehicle) + if isinstance(marker.unit, Spawn): + self.cc2me.remove_spawn(marker.object.object.data.respawn_id) + marker.delete() self.select_none() diff --git a/cc2me/web/__init__.py b/cc2me/web/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cc2me/web/__main__.py b/cc2me/web/__main__.py new file mode 100644 index 0000000..be01b2b --- /dev/null +++ b/cc2me/web/__main__.py @@ -0,0 +1,3 @@ +from .tool import run + +run() \ No newline at end of file diff --git a/cc2me/web/server.py b/cc2me/web/server.py new file mode 100644 index 0000000..29d5094 --- /dev/null +++ b/cc2me/web/server.py @@ -0,0 +1,124 @@ +"""Flask webserver for CC2E""" +from pathlib import Path + +from flask import Flask, render_template, request +from functools import lru_cache +from ..savedata.constants import read_save_slots, get_cc2_appdata, get_island_name +from ..savedata.loader import CC2XMLSave, load_save_file +from ..ui.cc2constants import get_team_color +from .utils import loc_to_geo_box, loc_to_geo, cc2_to_minutes, latlong_to_cc2loc, make_geodata + +WEB_DIR = Path(__file__).parent.absolute() +STATIC_DIR = WEB_DIR / "static" +TEMPLATE_DIR = WEB_DIR / "templates" + + +app = Flask("CC2E") +app.template_folder = TEMPLATE_DIR +app.static_folder = STATIC_DIR + + +@app.route("/") +def home(): + return render_template("index.html") + + +@app.route("/slots") +def slots_route(): + slots = read_save_slots() + return render_template("slots.html", slots=slots) + + +@lru_cache(4) +def get_map_slot(slot) -> CC2XMLSave: + path = Path(get_cc2_appdata()) / "saved_games" / slot / "save.xml" + save = load_save_file(path) + return save + + +@app.route("/geo//", defaults={"specific_id": -1}) +@app.route("/geo///") +def geojson_route(slot: str, layer: str, specific_id: int = -1): + save = get_map_slot(slot) + geo = { + "type": "FeatureCollection", + "features": [], + } + if layer == "spawns": + for tile in save.tiles: + for vspawn in tile.spawn_data.vehicles.items(): + if specific_id < 0 or specific_id == vspawn.data.respawn_id: + item = make_geodata( + id=vspawn.data.respawn_id, + properties={ + "kind": "vehicle_spawn", + "team": tile.spawn_data.team_id, + "team_color": get_team_color(tile.team_control), + "definition_index": vspawn.data.definition_index, + "hitpoints": vspawn.data.hitpoints, + "tile": tile.id + }, + geometry={ + "type": "Point", + "coordinates": loc_to_geo(vspawn.data.world_position) + } + ) + geo["features"].append(item) + if specific_id >= 0: + break + + elif layer == "tiles": + for tile in save.tiles: + if specific_id < 0 or specific_id == tile.id: + item = make_geodata( + id=tile.id, + properties={ + "kind": "tile", + "name": get_island_name(tile.id), + "team": tile.team_control, + "team_color": get_team_color(tile.team_control), + "difficulty": tile.difficulty_factor, + "facility.category": tile.facility.category, + "radius": cc2_to_minutes(tile.island_radius), + "loc": loc_to_geo(tile.loc), + }, + geometry={ + "type": "Polygon", + "coordinates": loc_to_geo_box(tile.loc, tile.island_radius, tile.island_radius) + } + ) + geo["features"].append(item) + if specific_id >= 0: + break + + return geo + + +@app.route("/map/") +def map_route(slot): + save = get_map_slot(slot) + return render_template("map.html", save=save, slot=slot) + + +@app.route("/move///", methods=["POST"]) +def move_item(slot: str, kind: str, item: int): + data = request.json + position = latlong_to_cc2loc(data["lat"], data["lng"]) + moved = [] + save = get_map_slot(slot) + if kind == "tile": + island = save.tile(item) + island.move(position.x, island.loc.y, position.z) + for spawn in island.spawn_data.vehicles.items(): + moved.append({"kind": "spawn", "id": spawn.data.respawn_id}) + + elif kind == "spawn": + # find the island with this spawn + found = save.spawn(item) + if found: + found.move(position.x, found.data.world_position.y, position.z) + moved.append({"kind": "spawn", "id": item}) + + return { + "moved": moved + } diff --git a/cc2me/web/static/Leaflet.Editable.js b/cc2me/web/static/Leaflet.Editable.js new file mode 100644 index 0000000..1bc352e --- /dev/null +++ b/cc2me/web/static/Leaflet.Editable.js @@ -0,0 +1,1946 @@ +'use strict'; +(function (factory, window) { + /*globals define, module, require*/ + + // define an AMD module that relies on 'leaflet' + if (typeof define === 'function' && define.amd) { + define(['leaflet'], factory); + + + // define a Common JS module that relies on 'leaflet' + } else if (typeof exports === 'object') { + module.exports = factory(require('leaflet')); + } + + // attach your plugin to the global 'L' variable + if(typeof window !== 'undefined' && window.L){ + factory(window.L); + } + +}(function (L) { + // 🍂miniclass CancelableEvent (Event objects) + // 🍂method cancel() + // Cancel any subsequent action. + + // 🍂miniclass VertexEvent (Event objects) + // 🍂property vertex: VertexMarker + // The vertex that fires the event. + + // 🍂miniclass ShapeEvent (Event objects) + // 🍂property shape: Array + // The shape (LatLngs array) subject of the action. + + // 🍂miniclass CancelableVertexEvent (Event objects) + // 🍂inherits VertexEvent + // 🍂inherits CancelableEvent + + // 🍂miniclass CancelableShapeEvent (Event objects) + // 🍂inherits ShapeEvent + // 🍂inherits CancelableEvent + + // 🍂miniclass LayerEvent (Event objects) + // 🍂property layer: object + // The Layer (Marker, Polyline…) subject of the action. + + // 🍂namespace Editable; 🍂class Editable; 🍂aka L.Editable + // Main edition handler. By default, it is attached to the map + // as `map.editTools` property. + // Leaflet.Editable is made to be fully extendable. You have three ways to customize + // the behaviour: using options, listening to events, or extending. + L.Editable = L.Evented.extend({ + + statics: { + FORWARD: 1, + BACKWARD: -1 + }, + + options: { + + // You can pass them when creating a map using the `editOptions` key. + // 🍂option zIndex: int = 1000 + // The default zIndex of the editing tools. + zIndex: 1000, + + // 🍂option polygonClass: class = L.Polygon + // Class to be used when creating a new Polygon. + polygonClass: L.Polygon, + + // 🍂option polylineClass: class = L.Polyline + // Class to be used when creating a new Polyline. + polylineClass: L.Polyline, + + // 🍂option markerClass: class = L.Marker + // Class to be used when creating a new Marker. + markerClass: L.Marker, + + // 🍂option rectangleClass: class = L.Rectangle + // Class to be used when creating a new Rectangle. + rectangleClass: L.Rectangle, + + // 🍂option circleClass: class = L.Circle + // Class to be used when creating a new Circle. + circleClass: L.Circle, + + // 🍂option drawingCSSClass: string = 'leaflet-editable-drawing' + // CSS class to be added to the map container while drawing. + drawingCSSClass: 'leaflet-editable-drawing', + + // 🍂option drawingCursor: const = 'crosshair' + // Cursor mode set to the map while drawing. + drawingCursor: 'crosshair', + + // 🍂option editLayer: Layer = new L.LayerGroup() + // Layer used to store edit tools (vertex, line guide…). + editLayer: undefined, + + // 🍂option featuresLayer: Layer = new L.LayerGroup() + // Default layer used to store drawn features (Marker, Polyline…). + featuresLayer: undefined, + + // 🍂option polylineEditorClass: class = PolylineEditor + // Class to be used as Polyline editor. + polylineEditorClass: undefined, + + // 🍂option polygonEditorClass: class = PolygonEditor + // Class to be used as Polygon editor. + polygonEditorClass: undefined, + + // 🍂option markerEditorClass: class = MarkerEditor + // Class to be used as Marker editor. + markerEditorClass: undefined, + + // 🍂option rectangleEditorClass: class = RectangleEditor + // Class to be used as Rectangle editor. + rectangleEditorClass: undefined, + + // 🍂option circleEditorClass: class = CircleEditor + // Class to be used as Circle editor. + circleEditorClass: undefined, + + // 🍂option lineGuideOptions: hash = {} + // Options to be passed to the line guides. + lineGuideOptions: {}, + + // 🍂option skipMiddleMarkers: boolean = false + // Set this to true if you don't want middle markers. + skipMiddleMarkers: false + + }, + + initialize: function (map, options) { + L.setOptions(this, options); + this._lastZIndex = this.options.zIndex; + this.map = map; + this.editLayer = this.createEditLayer(); + this.featuresLayer = this.createFeaturesLayer(); + this.forwardLineGuide = this.createLineGuide(); + this.backwardLineGuide = this.createLineGuide(); + }, + + fireAndForward: function (type, e) { + e = e || {}; + e.editTools = this; + this.fire(type, e); + this.map.fire(type, e); + }, + + createLineGuide: function () { + var options = L.extend({dashArray: '5,10', weight: 1, interactive: false}, this.options.lineGuideOptions); + return L.polyline([], options); + }, + + createVertexIcon: function (options) { + return L.Browser.mobile && L.Browser.touch ? new L.Editable.TouchVertexIcon(options) : new L.Editable.VertexIcon(options); + }, + + createEditLayer: function () { + return this.options.editLayer || new L.LayerGroup().addTo(this.map); + }, + + createFeaturesLayer: function () { + return this.options.featuresLayer || new L.LayerGroup().addTo(this.map); + }, + + moveForwardLineGuide: function (latlng) { + if (this.forwardLineGuide._latlngs.length) { + this.forwardLineGuide._latlngs[1] = latlng; + this.forwardLineGuide._bounds.extend(latlng); + this.forwardLineGuide.redraw(); + } + }, + + moveBackwardLineGuide: function (latlng) { + if (this.backwardLineGuide._latlngs.length) { + this.backwardLineGuide._latlngs[1] = latlng; + this.backwardLineGuide._bounds.extend(latlng); + this.backwardLineGuide.redraw(); + } + }, + + anchorForwardLineGuide: function (latlng) { + this.forwardLineGuide._latlngs[0] = latlng; + this.forwardLineGuide._bounds.extend(latlng); + this.forwardLineGuide.redraw(); + }, + + anchorBackwardLineGuide: function (latlng) { + this.backwardLineGuide._latlngs[0] = latlng; + this.backwardLineGuide._bounds.extend(latlng); + this.backwardLineGuide.redraw(); + }, + + attachForwardLineGuide: function () { + this.editLayer.addLayer(this.forwardLineGuide); + }, + + attachBackwardLineGuide: function () { + this.editLayer.addLayer(this.backwardLineGuide); + }, + + detachForwardLineGuide: function () { + this.forwardLineGuide.setLatLngs([]); + this.editLayer.removeLayer(this.forwardLineGuide); + }, + + detachBackwardLineGuide: function () { + this.backwardLineGuide.setLatLngs([]); + this.editLayer.removeLayer(this.backwardLineGuide); + }, + + blockEvents: function () { + // Hack: force map not to listen to other layers events while drawing. + if (!this._oldTargets) { + this._oldTargets = this.map._targets; + this.map._targets = {}; + } + }, + + unblockEvents: function () { + if (this._oldTargets) { + // Reset, but keep targets created while drawing. + this.map._targets = L.extend(this.map._targets, this._oldTargets); + delete this._oldTargets; + } + }, + + registerForDrawing: function (editor) { + if (this._drawingEditor) this.unregisterForDrawing(this._drawingEditor); + this.blockEvents(); + editor.reset(); // Make sure editor tools still receive events. + this._drawingEditor = editor; + this.map.on('mousemove touchmove', editor.onDrawingMouseMove, editor); + this.map.on('mousedown', this.onMousedown, this); + this.map.on('mouseup', this.onMouseup, this); + L.DomUtil.addClass(this.map._container, this.options.drawingCSSClass); + this.defaultMapCursor = this.map._container.style.cursor; + this.map._container.style.cursor = this.options.drawingCursor; + }, + + unregisterForDrawing: function (editor) { + this.unblockEvents(); + L.DomUtil.removeClass(this.map._container, this.options.drawingCSSClass); + this.map._container.style.cursor = this.defaultMapCursor; + editor = editor || this._drawingEditor; + if (!editor) return; + this.map.off('mousemove touchmove', editor.onDrawingMouseMove, editor); + this.map.off('mousedown', this.onMousedown, this); + this.map.off('mouseup', this.onMouseup, this); + if (editor !== this._drawingEditor) return; + delete this._drawingEditor; + if (editor._drawing) editor.cancelDrawing(); + }, + + onMousedown: function (e) { + if (e.originalEvent.which != 1) return; + this._mouseDown = e; + this._drawingEditor.onDrawingMouseDown(e); + }, + + onMouseup: function (e) { + if (this._mouseDown) { + var editor = this._drawingEditor, + mouseDown = this._mouseDown; + this._mouseDown = null; + editor.onDrawingMouseUp(e); + if (this._drawingEditor !== editor) return; // onDrawingMouseUp may call unregisterFromDrawing. + var origin = L.point(mouseDown.originalEvent.clientX, mouseDown.originalEvent.clientY); + var distance = L.point(e.originalEvent.clientX, e.originalEvent.clientY).distanceTo(origin); + if (Math.abs(distance) < 9 * (window.devicePixelRatio || 1)) this._drawingEditor.onDrawingClick(e); + } + }, + + // 🍂section Public methods + // You will generally access them by the `map.editTools` + // instance: + // + // `map.editTools.startPolyline();` + + // 🍂method drawing(): boolean + // Return true if any drawing action is ongoing. + drawing: function () { + return this._drawingEditor && this._drawingEditor.drawing(); + }, + + // 🍂method stopDrawing() + // When you need to stop any ongoing drawing, without needing to know which editor is active. + stopDrawing: function () { + this.unregisterForDrawing(); + }, + + // 🍂method commitDrawing() + // When you need to commit any ongoing drawing, without needing to know which editor is active. + commitDrawing: function (e) { + if (!this._drawingEditor) return; + this._drawingEditor.commitDrawing(e); + }, + + connectCreatedToMap: function (layer) { + return this.featuresLayer.addLayer(layer); + }, + + // 🍂method startPolyline(latlng: L.LatLng, options: hash): L.Polyline + // Start drawing a Polyline. If `latlng` is given, a first point will be added. In any case, continuing on user click. + // If `options` is given, it will be passed to the Polyline class constructor. + startPolyline: function (latlng, options) { + var line = this.createPolyline([], options); + line.enableEdit(this.map).newShape(latlng); + return line; + }, + + // 🍂method startPolygon(latlng: L.LatLng, options: hash): L.Polygon + // Start drawing a Polygon. If `latlng` is given, a first point will be added. In any case, continuing on user click. + // If `options` is given, it will be passed to the Polygon class constructor. + startPolygon: function (latlng, options) { + var polygon = this.createPolygon([], options); + polygon.enableEdit(this.map).newShape(latlng); + return polygon; + }, + + // 🍂method startMarker(latlng: L.LatLng, options: hash): L.Marker + // Start adding a Marker. If `latlng` is given, the Marker will be shown first at this point. + // In any case, it will follow the user mouse, and will have a final `latlng` on next click (or touch). + // If `options` is given, it will be passed to the Marker class constructor. + startMarker: function (latlng, options) { + latlng = latlng || this.map.getCenter().clone(); + var marker = this.createMarker(latlng, options); + marker.enableEdit(this.map).startDrawing(); + return marker; + }, + + // 🍂method startRectangle(latlng: L.LatLng, options: hash): L.Rectangle + // Start drawing a Rectangle. If `latlng` is given, the Rectangle anchor will be added. In any case, continuing on user drag. + // If `options` is given, it will be passed to the Rectangle class constructor. + startRectangle: function(latlng, options) { + var corner = latlng || L.latLng([0, 0]); + var bounds = new L.LatLngBounds(corner, corner); + var rectangle = this.createRectangle(bounds, options); + rectangle.enableEdit(this.map).startDrawing(); + return rectangle; + }, + + // 🍂method startCircle(latlng: L.LatLng, options: hash): L.Circle + // Start drawing a Circle. If `latlng` is given, the Circle anchor will be added. In any case, continuing on user drag. + // If `options` is given, it will be passed to the Circle class constructor. + startCircle: function (latlng, options) { + latlng = latlng || this.map.getCenter().clone(); + var circle = this.createCircle(latlng, options); + circle.enableEdit(this.map).startDrawing(); + return circle; + }, + + startHole: function (editor, latlng) { + editor.newHole(latlng); + }, + + createLayer: function (klass, latlngs, options) { + options = L.Util.extend({editOptions: {editTools: this}}, options); + var layer = new klass(latlngs, options); + // 🍂namespace Editable + // 🍂event editable:created: LayerEvent + // Fired when a new feature (Marker, Polyline…) is created. + this.fireAndForward('editable:created', {layer: layer}); + return layer; + }, + + createPolyline: function (latlngs, options) { + return this.createLayer(options && options.polylineClass || this.options.polylineClass, latlngs, options); + }, + + createPolygon: function (latlngs, options) { + return this.createLayer(options && options.polygonClass || this.options.polygonClass, latlngs, options); + }, + + createMarker: function (latlng, options) { + return this.createLayer(options && options.markerClass || this.options.markerClass, latlng, options); + }, + + createRectangle: function (bounds, options) { + return this.createLayer(options && options.rectangleClass || this.options.rectangleClass, bounds, options); + }, + + createCircle: function (latlng, options) { + return this.createLayer(options && options.circleClass || this.options.circleClass, latlng, options); + } + + }); + + L.extend(L.Editable, { + + makeCancellable: function (e) { + e.cancel = function () { + e._cancelled = true; + }; + } + + }); + + // 🍂namespace Map; 🍂class Map + // Leaflet.Editable add options and events to the `L.Map` object. + // See `Editable` events for the list of events fired on the Map. + // 🍂example + // + // ```js + // var map = L.map('map', { + // editable: true, + // editOptions: { + // … + // } + // }); + // ``` + // 🍂section Editable Map Options + L.Map.mergeOptions({ + + // 🍂namespace Map + // 🍂section Map Options + // 🍂option editToolsClass: class = L.Editable + // Class to be used as vertex, for path editing. + editToolsClass: L.Editable, + + // 🍂option editable: boolean = false + // Whether to create a L.Editable instance at map init. + editable: false, + + // 🍂option editOptions: hash = {} + // Options to pass to L.Editable when instantiating. + editOptions: {} + + }); + + L.Map.addInitHook(function () { + + this.whenReady(function () { + if (this.options.editable) { + this.editTools = new this.options.editToolsClass(this, this.options.editOptions); + } + }); + + }); + + L.Editable.VertexIcon = L.DivIcon.extend({ + + options: { + iconSize: new L.Point(8, 8) + } + + }); + + L.Editable.TouchVertexIcon = L.Editable.VertexIcon.extend({ + + options: { + iconSize: new L.Point(20, 20) + } + + }); + + + // 🍂namespace Editable; 🍂class VertexMarker; Handler for dragging path vertices. + L.Editable.VertexMarker = L.Marker.extend({ + + options: { + draggable: true, + className: 'leaflet-div-icon leaflet-vertex-icon' + }, + + + // 🍂section Public methods + // The marker used to handle path vertex. You will usually interact with a `VertexMarker` + // instance when listening for events like `editable:vertex:ctrlclick`. + + initialize: function (latlng, latlngs, editor, options) { + // We don't use this._latlng, because on drag Leaflet replace it while + // we want to keep reference. + this.latlng = latlng; + this.latlngs = latlngs; + this.editor = editor; + L.Marker.prototype.initialize.call(this, latlng, options); + this.options.icon = this.editor.tools.createVertexIcon({className: this.options.className}); + this.latlng.__vertex = this; + this.editor.editLayer.addLayer(this); + this.setZIndexOffset(editor.tools._lastZIndex + 1); + }, + + onAdd: function (map) { + L.Marker.prototype.onAdd.call(this, map); + this.on('drag', this.onDrag); + this.on('dragstart', this.onDragStart); + this.on('dragend', this.onDragEnd); + this.on('mouseup', this.onMouseup); + this.on('click', this.onClick); + this.on('contextmenu', this.onContextMenu); + this.on('mousedown touchstart', this.onMouseDown); + this.on('mouseover', this.onMouseOver); + this.on('mouseout', this.onMouseOut); + this.addMiddleMarkers(); + }, + + onRemove: function (map) { + if (this.middleMarker) this.middleMarker.delete(); + delete this.latlng.__vertex; + this.off('drag', this.onDrag); + this.off('dragstart', this.onDragStart); + this.off('dragend', this.onDragEnd); + this.off('mouseup', this.onMouseup); + this.off('click', this.onClick); + this.off('contextmenu', this.onContextMenu); + this.off('mousedown touchstart', this.onMouseDown); + this.off('mouseover', this.onMouseOver); + this.off('mouseout', this.onMouseOut); + L.Marker.prototype.onRemove.call(this, map); + }, + + onDrag: function (e) { + e.vertex = this; + this.editor.onVertexMarkerDrag(e); + var iconPos = L.DomUtil.getPosition(this._icon), + latlng = this._map.layerPointToLatLng(iconPos); + this.latlng.update(latlng); + this._latlng = this.latlng; // Push back to Leaflet our reference. + this.editor.refresh(); + if (this.middleMarker) this.middleMarker.updateLatLng(); + var next = this.getNext(); + if (next && next.middleMarker) next.middleMarker.updateLatLng(); + }, + + onDragStart: function (e) { + e.vertex = this; + this.editor.onVertexMarkerDragStart(e); + }, + + onDragEnd: function (e) { + e.vertex = this; + this.editor.onVertexMarkerDragEnd(e); + }, + + onClick: function (e) { + e.vertex = this; + this.editor.onVertexMarkerClick(e); + }, + + onMouseup: function (e) { + L.DomEvent.stop(e); + e.vertex = this; + this.editor.map.fire('mouseup', e); + }, + + onContextMenu: function (e) { + e.vertex = this; + this.editor.onVertexMarkerContextMenu(e); + }, + + onMouseDown: function (e) { + e.vertex = this; + this.editor.onVertexMarkerMouseDown(e); + }, + + onMouseOver: function (e) { + e.vertex = this; + this.editor.onVertexMarkerMouseOver(e); + }, + + onMouseOut: function (e) { + e.vertex = this; + this.editor.onVertexMarkerMouseOut(e); + }, + + // 🍂method delete() + // Delete a vertex and the related LatLng. + delete: function () { + var next = this.getNext(); // Compute before changing latlng + this.latlngs.splice(this.getIndex(), 1); + this.editor.editLayer.removeLayer(this); + this.editor.onVertexDeleted({latlng: this.latlng, vertex: this}); + if (!this.latlngs.length) this.editor.deleteShape(this.latlngs); + if (next) next.resetMiddleMarker(); + this.editor.refresh(); + }, + + // 🍂method getIndex(): int + // Get the index of the current vertex among others of the same LatLngs group. + getIndex: function () { + return this.latlngs.indexOf(this.latlng); + }, + + // 🍂method getLastIndex(): int + // Get last vertex index of the LatLngs group of the current vertex. + getLastIndex: function () { + return this.latlngs.length - 1; + }, + + // 🍂method getPrevious(): VertexMarker + // Get the previous VertexMarker in the same LatLngs group. + getPrevious: function () { + if (this.latlngs.length < 2) return; + var index = this.getIndex(), + previousIndex = index - 1; + if (index === 0 && this.editor.CLOSED) previousIndex = this.getLastIndex(); + var previous = this.latlngs[previousIndex]; + if (previous) return previous.__vertex; + }, + + // 🍂method getNext(): VertexMarker + // Get the next VertexMarker in the same LatLngs group. + getNext: function () { + if (this.latlngs.length < 2) return; + var index = this.getIndex(), + nextIndex = index + 1; + if (index === this.getLastIndex() && this.editor.CLOSED) nextIndex = 0; + var next = this.latlngs[nextIndex]; + if (next) return next.__vertex; + }, + + addMiddleMarker: function (previous) { + if (!this.editor.hasMiddleMarkers()) return; + previous = previous || this.getPrevious(); + if (previous && !this.middleMarker) this.middleMarker = this.editor.addMiddleMarker(previous, this, this.latlngs, this.editor); + }, + + addMiddleMarkers: function () { + if (!this.editor.hasMiddleMarkers()) return; + var previous = this.getPrevious(); + if (previous) this.addMiddleMarker(previous); + var next = this.getNext(); + if (next) next.resetMiddleMarker(); + }, + + resetMiddleMarker: function () { + if (this.middleMarker) this.middleMarker.delete(); + this.addMiddleMarker(); + }, + + // 🍂method split() + // Split the vertex LatLngs group at its index, if possible. + split: function () { + if (!this.editor.splitShape) return; // Only for PolylineEditor + this.editor.splitShape(this.latlngs, this.getIndex()); + }, + + // 🍂method continue() + // Continue the vertex LatLngs from this vertex. Only active for first and last vertices of a Polyline. + continue: function () { + if (!this.editor.continueBackward) return; // Only for PolylineEditor + var index = this.getIndex(); + if (index === 0) this.editor.continueBackward(this.latlngs); + else if (index === this.getLastIndex()) this.editor.continueForward(this.latlngs); + } + + }); + + L.Editable.mergeOptions({ + + // 🍂namespace Editable + // 🍂option vertexMarkerClass: class = VertexMarker + // Class to be used as vertex, for path editing. + vertexMarkerClass: L.Editable.VertexMarker + + }); + + L.Editable.MiddleMarker = L.Marker.extend({ + + options: { + opacity: 0.5, + className: 'leaflet-div-icon leaflet-middle-icon', + draggable: true + }, + + initialize: function (left, right, latlngs, editor, options) { + this.left = left; + this.right = right; + this.editor = editor; + this.latlngs = latlngs; + L.Marker.prototype.initialize.call(this, this.computeLatLng(), options); + this._opacity = this.options.opacity; + this.options.icon = this.editor.tools.createVertexIcon({className: this.options.className}); + this.editor.editLayer.addLayer(this); + this.setVisibility(); + }, + + setVisibility: function () { + var leftPoint = this._map.latLngToContainerPoint(this.left.latlng), + rightPoint = this._map.latLngToContainerPoint(this.right.latlng), + size = L.point(this.options.icon.options.iconSize); + if (leftPoint.distanceTo(rightPoint) < size.x * 3) this.hide(); + else this.show(); + }, + + show: function () { + this.setOpacity(this._opacity); + }, + + hide: function () { + this.setOpacity(0); + }, + + updateLatLng: function () { + this.setLatLng(this.computeLatLng()); + this.setVisibility(); + }, + + computeLatLng: function () { + var leftPoint = this.editor.map.latLngToContainerPoint(this.left.latlng), + rightPoint = this.editor.map.latLngToContainerPoint(this.right.latlng), + y = (leftPoint.y + rightPoint.y) / 2, + x = (leftPoint.x + rightPoint.x) / 2; + return this.editor.map.containerPointToLatLng([x, y]); + }, + + onAdd: function (map) { + L.Marker.prototype.onAdd.call(this, map); + L.DomEvent.on(this._icon, 'mousedown touchstart', this.onMouseDown, this); + map.on('zoomend', this.setVisibility, this); + }, + + onRemove: function (map) { + delete this.right.middleMarker; + L.DomEvent.off(this._icon, 'mousedown touchstart', this.onMouseDown, this); + map.off('zoomend', this.setVisibility, this); + L.Marker.prototype.onRemove.call(this, map); + }, + + onMouseDown: function (e) { + var iconPos = L.DomUtil.getPosition(this._icon), + latlng = this.editor.map.layerPointToLatLng(iconPos); + e = { + originalEvent: e, + latlng: latlng + }; + if (this.options.opacity === 0) return; + L.Editable.makeCancellable(e); + this.editor.onMiddleMarkerMouseDown(e); + if (e._cancelled) return; + this.latlngs.splice(this.index(), 0, e.latlng); + this.editor.refresh(); + var icon = this._icon; + var marker = this.editor.addVertexMarker(e.latlng, this.latlngs); + this.editor.onNewVertex(marker); + /* Hack to workaround browser not firing touchend when element is no more on DOM */ + var parent = marker._icon.parentNode; + parent.removeChild(marker._icon); + marker._icon = icon; + parent.appendChild(marker._icon); + marker._initIcon(); + marker._initInteraction(); + marker.setOpacity(1); + /* End hack */ + // Transfer ongoing dragging to real marker + L.Draggable._dragging = false; + marker.dragging._draggable._onDown(e.originalEvent); + this.delete(); + }, + + delete: function () { + this.editor.editLayer.removeLayer(this); + }, + + index: function () { + return this.latlngs.indexOf(this.right.latlng); + } + + }); + + L.Editable.mergeOptions({ + + // 🍂namespace Editable + // 🍂option middleMarkerClass: class = VertexMarker + // Class to be used as middle vertex, pulled by the user to create a new point in the middle of a path. + middleMarkerClass: L.Editable.MiddleMarker + + }); + + // 🍂namespace Editable; 🍂class BaseEditor; 🍂aka L.Editable.BaseEditor + // When editing a feature (Marker, Polyline…), an editor is attached to it. This + // editor basically knows how to handle the edition. + L.Editable.BaseEditor = L.Handler.extend({ + + initialize: function (map, feature, options) { + L.setOptions(this, options); + this.map = map; + this.feature = feature; + this.feature.editor = this; + this.editLayer = new L.LayerGroup(); + this.tools = this.options.editTools || map.editTools; + }, + + // 🍂method enable(): this + // Set up the drawing tools for the feature to be editable. + addHooks: function () { + if (this.isConnected()) this.onFeatureAdd(); + else this.feature.once('add', this.onFeatureAdd, this); + this.onEnable(); + this.feature.on(this._getEvents(), this); + }, + + // 🍂method disable(): this + // Remove the drawing tools for the feature. + removeHooks: function () { + this.feature.off(this._getEvents(), this); + if (this.feature.dragging) this.feature.dragging.disable(); + this.editLayer.clearLayers(); + this.tools.editLayer.removeLayer(this.editLayer); + this.onDisable(); + if (this._drawing) this.cancelDrawing(); + }, + + // 🍂method drawing(): boolean + // Return true if any drawing action is ongoing with this editor. + drawing: function () { + return !!this._drawing; + }, + + reset: function () {}, + + onFeatureAdd: function () { + this.tools.editLayer.addLayer(this.editLayer); + if (this.feature.dragging) this.feature.dragging.enable(); + }, + + hasMiddleMarkers: function () { + return !this.options.skipMiddleMarkers && !this.tools.options.skipMiddleMarkers; + }, + + fireAndForward: function (type, e) { + e = e || {}; + e.layer = this.feature; + this.feature.fire(type, e); + this.tools.fireAndForward(type, e); + }, + + onEnable: function () { + // 🍂namespace Editable + // 🍂event editable:enable: Event + // Fired when an existing feature is ready to be edited. + this.fireAndForward('editable:enable'); + }, + + onDisable: function () { + // 🍂namespace Editable + // 🍂event editable:disable: Event + // Fired when an existing feature is not ready anymore to be edited. + this.fireAndForward('editable:disable'); + }, + + onEditing: function () { + // 🍂namespace Editable + // 🍂event editable:editing: Event + // Fired as soon as any change is made to the feature geometry. + this.fireAndForward('editable:editing'); + }, + + onStartDrawing: function () { + // 🍂namespace Editable + // 🍂section Drawing events + // 🍂event editable:drawing:start: Event + // Fired when a feature is to be drawn. + this.fireAndForward('editable:drawing:start'); + }, + + onEndDrawing: function () { + // 🍂namespace Editable + // 🍂section Drawing events + // 🍂event editable:drawing:end: Event + // Fired when a feature is not drawn anymore. + this.fireAndForward('editable:drawing:end'); + }, + + onCancelDrawing: function () { + // 🍂namespace Editable + // 🍂section Drawing events + // 🍂event editable:drawing:cancel: Event + // Fired when user cancel drawing while a feature is being drawn. + this.fireAndForward('editable:drawing:cancel'); + }, + + onCommitDrawing: function (e) { + // 🍂namespace Editable + // 🍂section Drawing events + // 🍂event editable:drawing:commit: Event + // Fired when user finish drawing a feature. + this.fireAndForward('editable:drawing:commit', e); + }, + + onDrawingMouseDown: function (e) { + // 🍂namespace Editable + // 🍂section Drawing events + // 🍂event editable:drawing:mousedown: Event + // Fired when user `mousedown` while drawing. + this.fireAndForward('editable:drawing:mousedown', e); + }, + + onDrawingMouseUp: function (e) { + // 🍂namespace Editable + // 🍂section Drawing events + // 🍂event editable:drawing:mouseup: Event + // Fired when user `mouseup` while drawing. + this.fireAndForward('editable:drawing:mouseup', e); + }, + + startDrawing: function () { + if (!this._drawing) this._drawing = L.Editable.FORWARD; + this.tools.registerForDrawing(this); + this.onStartDrawing(); + }, + + commitDrawing: function (e) { + this.onCommitDrawing(e); + this.endDrawing(); + }, + + cancelDrawing: function () { + // If called during a vertex drag, the vertex will be removed before + // the mouseup fires on it. This is a workaround. Maybe better fix is + // To have L.Draggable reset it's status on disable (Leaflet side). + L.Draggable._dragging = false; + this.onCancelDrawing(); + this.endDrawing(); + }, + + endDrawing: function () { + this._drawing = false; + this.tools.unregisterForDrawing(this); + this.onEndDrawing(); + }, + + onDrawingClick: function (e) { + if (!this.drawing()) return; + L.Editable.makeCancellable(e); + // 🍂namespace Editable + // 🍂section Drawing events + // 🍂event editable:drawing:click: CancelableEvent + // Fired when user `click` while drawing, before any internal action is being processed. + this.fireAndForward('editable:drawing:click', e); + if (e._cancelled) return; + if (!this.isConnected()) this.connect(e); + this.processDrawingClick(e); + }, + + isConnected: function () { + return this.map.hasLayer(this.feature); + }, + + connect: function () { + this.tools.connectCreatedToMap(this.feature); + this.tools.editLayer.addLayer(this.editLayer); + }, + + onMove: function (e) { + // 🍂namespace Editable + // 🍂section Drawing events + // 🍂event editable:drawing:move: Event + // Fired when `move` mouse while drawing, while dragging a marker, and while dragging a vertex. + this.fireAndForward('editable:drawing:move', e); + }, + + onDrawingMouseMove: function (e) { + this.onMove(e); + }, + + _getEvents: function () { + return { + dragstart: this.onDragStart, + drag: this.onDrag, + dragend: this.onDragEnd, + remove: this.disable + }; + }, + + onDragStart: function (e) { + this.onEditing(); + // 🍂namespace Editable + // 🍂event editable:dragstart: Event + // Fired before a path feature is dragged. + this.fireAndForward('editable:dragstart', e); + }, + + onDrag: function (e) { + this.onMove(e); + // 🍂namespace Editable + // 🍂event editable:drag: Event + // Fired when a path feature is being dragged. + this.fireAndForward('editable:drag', e); + }, + + onDragEnd: function (e) { + // 🍂namespace Editable + // 🍂event editable:dragend: Event + // Fired after a path feature has been dragged. + this.fireAndForward('editable:dragend', e); + } + + }); + + // 🍂namespace Editable; 🍂class MarkerEditor; 🍂aka L.Editable.MarkerEditor + // 🍂inherits BaseEditor + // Editor for Marker. + L.Editable.MarkerEditor = L.Editable.BaseEditor.extend({ + + onDrawingMouseMove: function (e) { + L.Editable.BaseEditor.prototype.onDrawingMouseMove.call(this, e); + if (this._drawing) this.feature.setLatLng(e.latlng); + }, + + processDrawingClick: function (e) { + // 🍂namespace Editable + // 🍂section Drawing events + // 🍂event editable:drawing:clicked: Event + // Fired when user `click` while drawing, after all internal actions. + this.fireAndForward('editable:drawing:clicked', e); + this.commitDrawing(e); + }, + + connect: function (e) { + // On touch, the latlng has not been updated because there is + // no mousemove. + if (e) this.feature._latlng = e.latlng; + L.Editable.BaseEditor.prototype.connect.call(this, e); + } + + }); + + // 🍂namespace Editable; 🍂class PathEditor; 🍂aka L.Editable.PathEditor + // 🍂inherits BaseEditor + // Base class for all path editors. + L.Editable.PathEditor = L.Editable.BaseEditor.extend({ + + CLOSED: false, + MIN_VERTEX: 2, + + addHooks: function () { + L.Editable.BaseEditor.prototype.addHooks.call(this); + if (this.feature) this.initVertexMarkers(); + return this; + }, + + initVertexMarkers: function (latlngs) { + if (!this.enabled()) return; + latlngs = latlngs || this.getLatLngs(); + if (isFlat(latlngs)) this.addVertexMarkers(latlngs); + else for (var i = 0; i < latlngs.length; i++) this.initVertexMarkers(latlngs[i]); + }, + + getLatLngs: function () { + return this.feature.getLatLngs(); + }, + + // 🍂method reset() + // Rebuild edit elements (Vertex, MiddleMarker, etc.). + reset: function () { + this.editLayer.clearLayers(); + this.initVertexMarkers(); + }, + + addVertexMarker: function (latlng, latlngs) { + return new this.tools.options.vertexMarkerClass(latlng, latlngs, this); + }, + + onNewVertex: function (vertex) { + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:new: VertexEvent + // Fired when a new vertex is created. + this.fireAndForward('editable:vertex:new', {latlng: vertex.latlng, vertex: vertex}); + }, + + addVertexMarkers: function (latlngs) { + for (var i = 0; i < latlngs.length; i++) { + this.addVertexMarker(latlngs[i], latlngs); + } + }, + + refreshVertexMarkers: function (latlngs) { + latlngs = latlngs || this.getDefaultLatLngs(); + for (var i = 0; i < latlngs.length; i++) { + latlngs[i].__vertex.update(); + } + }, + + addMiddleMarker: function (left, right, latlngs) { + return new this.tools.options.middleMarkerClass(left, right, latlngs, this); + }, + + onVertexMarkerClick: function (e) { + L.Editable.makeCancellable(e); + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:click: CancelableVertexEvent + // Fired when a `click` is issued on a vertex, before any internal action is being processed. + this.fireAndForward('editable:vertex:click', e); + if (e._cancelled) return; + if (this.tools.drawing() && this.tools._drawingEditor !== this) return; + var index = e.vertex.getIndex(), commit; + if (e.originalEvent.ctrlKey) { + this.onVertexMarkerCtrlClick(e); + } else if (e.originalEvent.altKey) { + this.onVertexMarkerAltClick(e); + } else if (e.originalEvent.shiftKey) { + this.onVertexMarkerShiftClick(e); + } else if (e.originalEvent.metaKey) { + this.onVertexMarkerMetaKeyClick(e); + } else if (index === e.vertex.getLastIndex() && this._drawing === L.Editable.FORWARD) { + if (index >= this.MIN_VERTEX - 1) commit = true; + } else if (index === 0 && this._drawing === L.Editable.BACKWARD && this._drawnLatLngs.length >= this.MIN_VERTEX) { + commit = true; + } else if (index === 0 && this._drawing === L.Editable.FORWARD && this._drawnLatLngs.length >= this.MIN_VERTEX && this.CLOSED) { + commit = true; // Allow to close on first point also for polygons + } else { + this.onVertexRawMarkerClick(e); + } + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:clicked: VertexEvent + // Fired when a `click` is issued on a vertex, after all internal actions. + this.fireAndForward('editable:vertex:clicked', e); + if (commit) this.commitDrawing(e); + }, + + onVertexRawMarkerClick: function (e) { + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:rawclick: CancelableVertexEvent + // Fired when a `click` is issued on a vertex without any special key and without being in drawing mode. + this.fireAndForward('editable:vertex:rawclick', e); + if (e._cancelled) return; + if (!this.vertexCanBeDeleted(e.vertex)) return; + e.vertex.delete(); + }, + + vertexCanBeDeleted: function (vertex) { + return vertex.latlngs.length > this.MIN_VERTEX; + }, + + onVertexDeleted: function (e) { + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:deleted: VertexEvent + // Fired after a vertex has been deleted by user. + this.fireAndForward('editable:vertex:deleted', e); + }, + + onVertexMarkerCtrlClick: function (e) { + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:ctrlclick: VertexEvent + // Fired when a `click` with `ctrlKey` is issued on a vertex. + this.fireAndForward('editable:vertex:ctrlclick', e); + }, + + onVertexMarkerShiftClick: function (e) { + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:shiftclick: VertexEvent + // Fired when a `click` with `shiftKey` is issued on a vertex. + this.fireAndForward('editable:vertex:shiftclick', e); + }, + + onVertexMarkerMetaKeyClick: function (e) { + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:metakeyclick: VertexEvent + // Fired when a `click` with `metaKey` is issued on a vertex. + this.fireAndForward('editable:vertex:metakeyclick', e); + }, + + onVertexMarkerAltClick: function (e) { + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:altclick: VertexEvent + // Fired when a `click` with `altKey` is issued on a vertex. + this.fireAndForward('editable:vertex:altclick', e); + }, + + onVertexMarkerContextMenu: function (e) { + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:contextmenu: VertexEvent + // Fired when a `contextmenu` is issued on a vertex. + this.fireAndForward('editable:vertex:contextmenu', e); + }, + + onVertexMarkerMouseDown: function (e) { + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:mousedown: VertexEvent + // Fired when user `mousedown` a vertex. + this.fireAndForward('editable:vertex:mousedown', e); + }, + + onVertexMarkerMouseOver: function (e) { + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:mouseover: VertexEvent + // Fired when a user's mouse enters the vertex + this.fireAndForward('editable:vertex:mouseover', e); + }, + + onVertexMarkerMouseOut: function (e) { + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:mouseout: VertexEvent + // Fired when a user's mouse leaves the vertex + this.fireAndForward('editable:vertex:mouseout', e); + }, + + onMiddleMarkerMouseDown: function (e) { + // 🍂namespace Editable + // 🍂section MiddleMarker events + // 🍂event editable:middlemarker:mousedown: VertexEvent + // Fired when user `mousedown` a middle marker. + this.fireAndForward('editable:middlemarker:mousedown', e); + }, + + onVertexMarkerDrag: function (e) { + this.onMove(e); + if (this.feature._bounds) this.extendBounds(e); + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:drag: VertexEvent + // Fired when a vertex is dragged by user. + this.fireAndForward('editable:vertex:drag', e); + }, + + onVertexMarkerDragStart: function (e) { + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:dragstart: VertexEvent + // Fired before a vertex is dragged by user. + this.fireAndForward('editable:vertex:dragstart', e); + }, + + onVertexMarkerDragEnd: function (e) { + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:dragend: VertexEvent + // Fired after a vertex is dragged by user. + this.fireAndForward('editable:vertex:dragend', e); + }, + + setDrawnLatLngs: function (latlngs) { + this._drawnLatLngs = latlngs || this.getDefaultLatLngs(); + }, + + startDrawing: function () { + if (!this._drawnLatLngs) this.setDrawnLatLngs(); + L.Editable.BaseEditor.prototype.startDrawing.call(this); + }, + + startDrawingForward: function () { + this.startDrawing(); + }, + + endDrawing: function () { + this.tools.detachForwardLineGuide(); + this.tools.detachBackwardLineGuide(); + if (this._drawnLatLngs && this._drawnLatLngs.length < this.MIN_VERTEX) this.deleteShape(this._drawnLatLngs); + L.Editable.BaseEditor.prototype.endDrawing.call(this); + delete this._drawnLatLngs; + }, + + addLatLng: function (latlng) { + if (this._drawing === L.Editable.FORWARD) this._drawnLatLngs.push(latlng); + else this._drawnLatLngs.unshift(latlng); + this.feature._bounds.extend(latlng); + var vertex = this.addVertexMarker(latlng, this._drawnLatLngs); + this.onNewVertex(vertex); + this.refresh(); + }, + + newPointForward: function (latlng) { + this.addLatLng(latlng); + this.tools.attachForwardLineGuide(); + this.tools.anchorForwardLineGuide(latlng); + }, + + newPointBackward: function (latlng) { + this.addLatLng(latlng); + this.tools.anchorBackwardLineGuide(latlng); + }, + + // 🍂namespace PathEditor + // 🍂method push() + // Programmatically add a point while drawing. + push: function (latlng) { + if (!latlng) return console.error('L.Editable.PathEditor.push expect a valid latlng as parameter'); + if (this._drawing === L.Editable.FORWARD) this.newPointForward(latlng); + else this.newPointBackward(latlng); + }, + + removeLatLng: function (latlng) { + latlng.__vertex.delete(); + this.refresh(); + }, + + // 🍂method pop(): L.LatLng or null + // Programmatically remove last point (if any) while drawing. + pop: function () { + if (this._drawnLatLngs.length <= 1) return; + var latlng; + if (this._drawing === L.Editable.FORWARD) latlng = this._drawnLatLngs[this._drawnLatLngs.length - 1]; + else latlng = this._drawnLatLngs[0]; + this.removeLatLng(latlng); + if (this._drawing === L.Editable.FORWARD) this.tools.anchorForwardLineGuide(this._drawnLatLngs[this._drawnLatLngs.length - 1]); + else this.tools.anchorForwardLineGuide(this._drawnLatLngs[0]); + return latlng; + }, + + processDrawingClick: function (e) { + if (e.vertex && e.vertex.editor === this) return; + if (this._drawing === L.Editable.FORWARD) this.newPointForward(e.latlng); + else this.newPointBackward(e.latlng); + this.fireAndForward('editable:drawing:clicked', e); + }, + + onDrawingMouseMove: function (e) { + L.Editable.BaseEditor.prototype.onDrawingMouseMove.call(this, e); + if (this._drawing) { + this.tools.moveForwardLineGuide(e.latlng); + this.tools.moveBackwardLineGuide(e.latlng); + } + }, + + refresh: function () { + this.feature.redraw(); + this.onEditing(); + }, + + // 🍂namespace PathEditor + // 🍂method newShape(latlng?: L.LatLng) + // Add a new shape (Polyline, Polygon) in a multi, and setup up drawing tools to draw it; + // if optional `latlng` is given, start a path at this point. + newShape: function (latlng) { + var shape = this.addNewEmptyShape(); + if (!shape) return; + this.setDrawnLatLngs(shape[0] || shape); // Polygon or polyline + this.startDrawingForward(); + // 🍂namespace Editable + // 🍂section Shape events + // 🍂event editable:shape:new: ShapeEvent + // Fired when a new shape is created in a multi (Polygon or Polyline). + this.fireAndForward('editable:shape:new', {shape: shape}); + if (latlng) this.newPointForward(latlng); + }, + + deleteShape: function (shape, latlngs) { + var e = {shape: shape}; + L.Editable.makeCancellable(e); + // 🍂namespace Editable + // 🍂section Shape events + // 🍂event editable:shape:delete: CancelableShapeEvent + // Fired before a new shape is deleted in a multi (Polygon or Polyline). + this.fireAndForward('editable:shape:delete', e); + if (e._cancelled) return; + shape = this._deleteShape(shape, latlngs); + if (this.ensureNotFlat) this.ensureNotFlat(); // Polygon. + this.feature.setLatLngs(this.getLatLngs()); // Force bounds reset. + this.refresh(); + this.reset(); + // 🍂namespace Editable + // 🍂section Shape events + // 🍂event editable:shape:deleted: ShapeEvent + // Fired after a new shape is deleted in a multi (Polygon or Polyline). + this.fireAndForward('editable:shape:deleted', {shape: shape}); + return shape; + }, + + _deleteShape: function (shape, latlngs) { + latlngs = latlngs || this.getLatLngs(); + if (!latlngs.length) return; + var self = this, + inplaceDelete = function (latlngs, shape) { + // Called when deleting a flat latlngs + shape = latlngs.splice(0, Number.MAX_VALUE); + return shape; + }, + spliceDelete = function (latlngs, shape) { + // Called when removing a latlngs inside an array + latlngs.splice(latlngs.indexOf(shape), 1); + if (!latlngs.length) self._deleteShape(latlngs); + return shape; + }; + if (latlngs === shape) return inplaceDelete(latlngs, shape); + for (var i = 0; i < latlngs.length; i++) { + if (latlngs[i] === shape) return spliceDelete(latlngs, shape); + else if (latlngs[i].indexOf(shape) !== -1) return spliceDelete(latlngs[i], shape); + } + }, + + // 🍂namespace PathEditor + // 🍂method deleteShapeAt(latlng: L.LatLng): Array + // Remove a path shape at the given `latlng`. + deleteShapeAt: function (latlng) { + var shape = this.feature.shapeAt(latlng); + if (shape) return this.deleteShape(shape); + }, + + // 🍂method appendShape(shape: Array) + // Append a new shape to the Polygon or Polyline. + appendShape: function (shape) { + this.insertShape(shape); + }, + + // 🍂method prependShape(shape: Array) + // Prepend a new shape to the Polygon or Polyline. + prependShape: function (shape) { + this.insertShape(shape, 0); + }, + + // 🍂method insertShape(shape: Array, index: int) + // Insert a new shape to the Polygon or Polyline at given index (default is to append). + insertShape: function (shape, index) { + this.ensureMulti(); + shape = this.formatShape(shape); + if (typeof index === 'undefined') index = this.feature._latlngs.length; + this.feature._latlngs.splice(index, 0, shape); + this.feature.redraw(); + if (this._enabled) this.reset(); + }, + + extendBounds: function (e) { + this.feature._bounds.extend(e.vertex.latlng); + }, + + onDragStart: function (e) { + this.editLayer.clearLayers(); + L.Editable.BaseEditor.prototype.onDragStart.call(this, e); + }, + + onDragEnd: function (e) { + this.initVertexMarkers(); + L.Editable.BaseEditor.prototype.onDragEnd.call(this, e); + } + + }); + + // 🍂namespace Editable; 🍂class PolylineEditor; 🍂aka L.Editable.PolylineEditor + // 🍂inherits PathEditor + L.Editable.PolylineEditor = L.Editable.PathEditor.extend({ + + startDrawingBackward: function () { + this._drawing = L.Editable.BACKWARD; + this.startDrawing(); + }, + + // 🍂method continueBackward(latlngs?: Array) + // Set up drawing tools to continue the line backward. + continueBackward: function (latlngs) { + if (this.drawing()) return; + latlngs = latlngs || this.getDefaultLatLngs(); + this.setDrawnLatLngs(latlngs); + if (latlngs.length > 0) { + this.tools.attachBackwardLineGuide(); + this.tools.anchorBackwardLineGuide(latlngs[0]); + } + this.startDrawingBackward(); + }, + + // 🍂method continueForward(latlngs?: Array) + // Set up drawing tools to continue the line forward. + continueForward: function (latlngs) { + if (this.drawing()) return; + latlngs = latlngs || this.getDefaultLatLngs(); + this.setDrawnLatLngs(latlngs); + if (latlngs.length > 0) { + this.tools.attachForwardLineGuide(); + this.tools.anchorForwardLineGuide(latlngs[latlngs.length - 1]); + } + this.startDrawingForward(); + }, + + getDefaultLatLngs: function (latlngs) { + latlngs = latlngs || this.feature._latlngs; + if (!latlngs.length || latlngs[0] instanceof L.LatLng) return latlngs; + else return this.getDefaultLatLngs(latlngs[0]); + }, + + ensureMulti: function () { + if (this.feature._latlngs.length && isFlat(this.feature._latlngs)) { + this.feature._latlngs = [this.feature._latlngs]; + } + }, + + addNewEmptyShape: function () { + if (this.feature._latlngs.length) { + var shape = []; + this.appendShape(shape); + return shape; + } else { + return this.feature._latlngs; + } + }, + + formatShape: function (shape) { + if (isFlat(shape)) return shape; + else if (shape[0]) return this.formatShape(shape[0]); + }, + + // 🍂method splitShape(latlngs?: Array, index: int) + // Split the given `latlngs` shape at index `index` and integrate new shape in instance `latlngs`. + splitShape: function (shape, index) { + if (!index || index >= shape.length - 1) return; + this.ensureMulti(); + var shapeIndex = this.feature._latlngs.indexOf(shape); + if (shapeIndex === -1) return; + var first = shape.slice(0, index + 1), + second = shape.slice(index); + // We deal with reference, we don't want twice the same latlng around. + second[0] = L.latLng(second[0].lat, second[0].lng, second[0].alt); + this.feature._latlngs.splice(shapeIndex, 1, first, second); + this.refresh(); + this.reset(); + } + + }); + + // 🍂namespace Editable; 🍂class PolygonEditor; 🍂aka L.Editable.PolygonEditor + // 🍂inherits PathEditor + L.Editable.PolygonEditor = L.Editable.PathEditor.extend({ + + CLOSED: true, + MIN_VERTEX: 3, + + newPointForward: function (latlng) { + L.Editable.PathEditor.prototype.newPointForward.call(this, latlng); + if (!this.tools.backwardLineGuide._latlngs.length) this.tools.anchorBackwardLineGuide(latlng); + if (this._drawnLatLngs.length === 2) this.tools.attachBackwardLineGuide(); + }, + + addNewEmptyHole: function (latlng) { + this.ensureNotFlat(); + var latlngs = this.feature.shapeAt(latlng); + if (!latlngs) return; + var holes = []; + latlngs.push(holes); + return holes; + }, + + // 🍂method newHole(latlng?: L.LatLng, index: int) + // Set up drawing tools for creating a new hole on the Polygon. If the `latlng` param is given, a first point is created. + newHole: function (latlng) { + var holes = this.addNewEmptyHole(latlng); + if (!holes) return; + this.setDrawnLatLngs(holes); + this.startDrawingForward(); + if (latlng) this.newPointForward(latlng); + }, + + addNewEmptyShape: function () { + if (this.feature._latlngs.length && this.feature._latlngs[0].length) { + var shape = []; + this.appendShape(shape); + return shape; + } else { + return this.feature._latlngs; + } + }, + + ensureMulti: function () { + if (this.feature._latlngs.length && isFlat(this.feature._latlngs[0])) { + this.feature._latlngs = [this.feature._latlngs]; + } + }, + + ensureNotFlat: function () { + if (!this.feature._latlngs.length || isFlat(this.feature._latlngs)) this.feature._latlngs = [this.feature._latlngs]; + }, + + vertexCanBeDeleted: function (vertex) { + var parent = this.feature.parentShape(vertex.latlngs), + idx = L.Util.indexOf(parent, vertex.latlngs); + if (idx > 0) return true; // Holes can be totally deleted without removing the layer itself. + return L.Editable.PathEditor.prototype.vertexCanBeDeleted.call(this, vertex); + }, + + getDefaultLatLngs: function () { + if (!this.feature._latlngs.length) this.feature._latlngs.push([]); + return this.feature._latlngs[0]; + }, + + formatShape: function (shape) { + // [[1, 2], [3, 4]] => must be nested + // [] => must be nested + // [[]] => is already nested + if (isFlat(shape) && (!shape[0] || shape[0].length !== 0)) return [shape]; + else return shape; + } + + }); + + // 🍂namespace Editable; 🍂class RectangleEditor; 🍂aka L.Editable.RectangleEditor + // 🍂inherits PathEditor + L.Editable.RectangleEditor = L.Editable.PathEditor.extend({ + + CLOSED: true, + MIN_VERTEX: 4, + + options: { + skipMiddleMarkers: true + }, + + extendBounds: function (e) { + var index = e.vertex.getIndex(), + next = e.vertex.getNext(), + previous = e.vertex.getPrevious(), + oppositeIndex = (index + 2) % 4, + opposite = e.vertex.latlngs[oppositeIndex], + bounds = new L.LatLngBounds(e.latlng, opposite); + // Update latlngs by hand to preserve order. + previous.latlng.update([e.latlng.lat, opposite.lng]); + next.latlng.update([opposite.lat, e.latlng.lng]); + this.updateBounds(bounds); + this.refreshVertexMarkers(); + }, + + onDrawingMouseDown: function (e) { + L.Editable.PathEditor.prototype.onDrawingMouseDown.call(this, e); + this.connect(); + var latlngs = this.getDefaultLatLngs(); + // L.Polygon._convertLatLngs removes last latlng if it equals first point, + // which is the case here as all latlngs are [0, 0] + if (latlngs.length === 3) latlngs.push(e.latlng); + var bounds = new L.LatLngBounds(e.latlng, e.latlng); + this.updateBounds(bounds); + this.updateLatLngs(bounds); + this.refresh(); + this.reset(); + // Stop dragging map. + // L.Draggable has two workflows: + // - mousedown => mousemove => mouseup + // - touchstart => touchmove => touchend + // Problem: L.Map.Tap does not allow us to listen to touchstart, so we only + // can deal with mousedown, but then when in a touch device, we are dealing with + // simulated events (actually simulated by L.Map.Tap), which are no more taken + // into account by L.Draggable. + // Ref.: https://github.com/Leaflet/Leaflet.Editable/issues/103 + e.originalEvent._simulated = false; + this.map.dragging._draggable._onUp(e.originalEvent); + // Now transfer ongoing drag action to the bottom right corner. + // Should we refine which corner will handle the drag according to + // drag direction? + latlngs[3].__vertex.dragging._draggable._onDown(e.originalEvent); + }, + + onDrawingMouseUp: function (e) { + this.commitDrawing(e); + e.originalEvent._simulated = false; + L.Editable.PathEditor.prototype.onDrawingMouseUp.call(this, e); + }, + + onDrawingMouseMove: function (e) { + e.originalEvent._simulated = false; + L.Editable.PathEditor.prototype.onDrawingMouseMove.call(this, e); + }, + + + getDefaultLatLngs: function (latlngs) { + return latlngs || this.feature._latlngs[0]; + }, + + updateBounds: function (bounds) { + this.feature._bounds = bounds; + }, + + updateLatLngs: function (bounds) { + var latlngs = this.getDefaultLatLngs(), + newLatlngs = this.feature._boundsToLatLngs(bounds); + // Keep references. + for (var i = 0; i < latlngs.length; i++) { + latlngs[i].update(newLatlngs[i]); + } + } + + }); + + // 🍂namespace Editable; 🍂class CircleEditor; 🍂aka L.Editable.CircleEditor + // 🍂inherits PathEditor + L.Editable.CircleEditor = L.Editable.PathEditor.extend({ + + MIN_VERTEX: 2, + + options: { + skipMiddleMarkers: true + }, + + initialize: function (map, feature, options) { + L.Editable.PathEditor.prototype.initialize.call(this, map, feature, options); + this._resizeLatLng = this.computeResizeLatLng(); + }, + + computeResizeLatLng: function () { + // While circle is not added to the map, _radius is not set. + var delta = (this.feature._radius || this.feature._mRadius) * Math.cos(Math.PI / 4), + point = this.map.project(this.feature._latlng); + return this.map.unproject([point.x + delta, point.y - delta]); + }, + + updateResizeLatLng: function () { + this._resizeLatLng.update(this.computeResizeLatLng()); + this._resizeLatLng.__vertex.update(); + }, + + getLatLngs: function () { + return [this.feature._latlng, this._resizeLatLng]; + }, + + getDefaultLatLngs: function () { + return this.getLatLngs(); + }, + + onVertexMarkerDrag: function (e) { + if (e.vertex.getIndex() === 1) this.resize(e); + else this.updateResizeLatLng(e); + L.Editable.PathEditor.prototype.onVertexMarkerDrag.call(this, e); + }, + + resize: function (e) { + var radius = this.feature._latlng.distanceTo(e.latlng); + this.feature.setRadius(radius); + }, + + onDrawingMouseDown: function (e) { + L.Editable.PathEditor.prototype.onDrawingMouseDown.call(this, e); + this._resizeLatLng.update(e.latlng); + this.feature._latlng.update(e.latlng); + this.connect(); + // Stop dragging map. + e.originalEvent._simulated = false; + this.map.dragging._draggable._onUp(e.originalEvent); + // Now transfer ongoing drag action to the radius handler. + this._resizeLatLng.__vertex.dragging._draggable._onDown(e.originalEvent); + }, + + onDrawingMouseUp: function (e) { + this.commitDrawing(e); + e.originalEvent._simulated = false; + L.Editable.PathEditor.prototype.onDrawingMouseUp.call(this, e); + }, + + onDrawingMouseMove: function (e) { + e.originalEvent._simulated = false; + L.Editable.PathEditor.prototype.onDrawingMouseMove.call(this, e); + }, + + onDrag: function (e) { + L.Editable.PathEditor.prototype.onDrag.call(this, e); + this.feature.dragging.updateLatLng(this._resizeLatLng); + } + + }); + + // 🍂namespace Editable; 🍂class EditableMixin + // `EditableMixin` is included to `L.Polyline`, `L.Polygon`, `L.Rectangle`, `L.Circle` + // and `L.Marker`. It adds some methods to them. + // *When editing is enabled, the editor is accessible on the instance with the + // `editor` property.* + var EditableMixin = { + + createEditor: function (map) { + map = map || this._map; + var tools = (this.options.editOptions || {}).editTools || map.editTools; + if (!tools) throw Error('Unable to detect Editable instance.'); + var Klass = this.options.editorClass || this.getEditorClass(tools); + return new Klass(map, this, this.options.editOptions); + }, + + // 🍂method enableEdit(map?: L.Map): this.editor + // Enable editing, by creating an editor if not existing, and then calling `enable` on it. + enableEdit: function (map) { + if (!this.editor) this.createEditor(map); + this.editor.enable(); + return this.editor; + }, + + // 🍂method editEnabled(): boolean + // Return true if current instance has an editor attached, and this editor is enabled. + editEnabled: function () { + return this.editor && this.editor.enabled(); + }, + + // 🍂method disableEdit() + // Disable editing, also remove the editor property reference. + disableEdit: function () { + if (this.editor) { + this.editor.disable(); + delete this.editor; + } + }, + + // 🍂method toggleEdit() + // Enable or disable editing, according to current status. + toggleEdit: function () { + if (this.editEnabled()) this.disableEdit(); + else this.enableEdit(); + }, + + _onEditableAdd: function () { + if (this.editor) this.enableEdit(); + } + + }; + + var PolylineMixin = { + + getEditorClass: function (tools) { + return (tools && tools.options.polylineEditorClass) ? tools.options.polylineEditorClass : L.Editable.PolylineEditor; + }, + + shapeAt: function (latlng, latlngs) { + // We can have those cases: + // - latlngs are just a flat array of latlngs, use this + // - latlngs is an array of arrays of latlngs, loop over + var shape = null; + latlngs = latlngs || this._latlngs; + if (!latlngs.length) return shape; + else if (isFlat(latlngs) && this.isInLatLngs(latlng, latlngs)) shape = latlngs; + else for (var i = 0; i < latlngs.length; i++) if (this.isInLatLngs(latlng, latlngs[i])) return latlngs[i]; + return shape; + }, + + isInLatLngs: function (l, latlngs) { + if (!latlngs) return false; + var i, k, len, part = [], p, + w = this._clickTolerance(); + this._projectLatlngs(latlngs, part, this._pxBounds); + part = part[0]; + p = this._map.latLngToLayerPoint(l); + + if (!this._pxBounds.contains(p)) { return false; } + for (i = 1, len = part.length, k = 0; i < len; k = i++) { + + if (L.LineUtil.pointToSegmentDistance(p, part[k], part[i]) <= w) { + return true; + } + } + return false; + } + + }; + + var PolygonMixin = { + + getEditorClass: function (tools) { + return (tools && tools.options.polygonEditorClass) ? tools.options.polygonEditorClass : L.Editable.PolygonEditor; + }, + + shapeAt: function (latlng, latlngs) { + // We can have those cases: + // - latlngs are just a flat array of latlngs, use this + // - latlngs is an array of arrays of latlngs, this is a simple polygon (maybe with holes), use the first + // - latlngs is an array of arrays of arrays, this is a multi, loop over + var shape = null; + latlngs = latlngs || this._latlngs; + if (!latlngs.length) return shape; + else if (isFlat(latlngs) && this.isInLatLngs(latlng, latlngs)) shape = latlngs; + else if (isFlat(latlngs[0]) && this.isInLatLngs(latlng, latlngs[0])) shape = latlngs; + else for (var i = 0; i < latlngs.length; i++) if (this.isInLatLngs(latlng, latlngs[i][0])) return latlngs[i]; + return shape; + }, + + isInLatLngs: function (l, latlngs) { + var inside = false, l1, l2, j, k, len2; + + for (j = 0, len2 = latlngs.length, k = len2 - 1; j < len2; k = j++) { + l1 = latlngs[j]; + l2 = latlngs[k]; + + if (((l1.lat > l.lat) !== (l2.lat > l.lat)) && + (l.lng < (l2.lng - l1.lng) * (l.lat - l1.lat) / (l2.lat - l1.lat) + l1.lng)) { + inside = !inside; + } + } + + return inside; + }, + + parentShape: function (shape, latlngs) { + latlngs = latlngs || this._latlngs; + if (!latlngs) return; + var idx = L.Util.indexOf(latlngs, shape); + if (idx !== -1) return latlngs; + for (var i = 0; i < latlngs.length; i++) { + idx = L.Util.indexOf(latlngs[i], shape); + if (idx !== -1) return latlngs[i]; + } + } + + }; + + + var MarkerMixin = { + + getEditorClass: function (tools) { + return (tools && tools.options.markerEditorClass) ? tools.options.markerEditorClass : L.Editable.MarkerEditor; + } + + }; + + var RectangleMixin = { + + getEditorClass: function (tools) { + return (tools && tools.options.rectangleEditorClass) ? tools.options.rectangleEditorClass : L.Editable.RectangleEditor; + } + + }; + + var CircleMixin = { + + getEditorClass: function (tools) { + return (tools && tools.options.circleEditorClass) ? tools.options.circleEditorClass : L.Editable.CircleEditor; + } + + }; + + var keepEditable = function () { + // Make sure you can remove/readd an editable layer. + this.on('add', this._onEditableAdd); + }; + + var isFlat = L.LineUtil.isFlat || L.LineUtil._flat || L.Polyline._flat; // <=> 1.1 compat. + + + if (L.Polyline) { + L.Polyline.include(EditableMixin); + L.Polyline.include(PolylineMixin); + L.Polyline.addInitHook(keepEditable); + } + if (L.Polygon) { + L.Polygon.include(EditableMixin); + L.Polygon.include(PolygonMixin); + } + if (L.Marker) { + L.Marker.include(EditableMixin); + L.Marker.include(MarkerMixin); + L.Marker.addInitHook(keepEditable); + } + if (L.Rectangle) { + L.Rectangle.include(EditableMixin); + L.Rectangle.include(RectangleMixin); + } + if (L.Circle) { + L.Circle.include(EditableMixin); + L.Circle.include(CircleMixin); + } + + L.LatLng.prototype.update = function (latlng) { + latlng = L.latLng(latlng); + this.lat = latlng.lat; + this.lng = latlng.lng; + } + +}, window)); \ No newline at end of file diff --git a/cc2me/web/static/Path.Drag.min.js b/cc2me/web/static/Path.Drag.min.js new file mode 100644 index 0000000..7a2f7fc --- /dev/null +++ b/cc2me/web/static/Path.Drag.min.js @@ -0,0 +1,8 @@ +/** + * Minified by jsDelivr using UglifyJS v3.1.10. + * Original file: /npm/leaflet.path.drag@0.0.6/src/Path.Drag.js + * + * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files + */ +"use strict";L.PathDraggable=L.Draggable.extend({initialize:function(t){this._path=t,this._canvas=t._map.getRenderer(t)instanceof L.Canvas;var a=this._canvas?this._path._map.getRenderer(this._path)._container:this._path._path;L.Draggable.prototype.initialize.call(this,a,a,!0)},_updatePosition:function(){var t={originalEvent:this._lastEvent};this.fire("drag",t)},_onDown:function(t){var a=t.touches?t.touches[0]:t;this._startPoint=new L.Point(a.clientX,a.clientY),this._canvas&&!this._path._containsPoint(this._path._map.mouseEventToLayerPoint(a))||L.Draggable.prototype._onDown.call(this,t)}}),L.Handler.PathDrag=L.Handler.extend({initialize:function(t){this._path=t},getEvents:function(){return{dragstart:this._onDragStart,drag:this._onDrag,dragend:this._onDragEnd}},addHooks:function(){this._draggable||(this._draggable=new L.PathDraggable(this._path)),this._draggable.on(this.getEvents(),this).enable(),L.DomUtil.addClass(this._draggable._element,"leaflet-path-draggable")},removeHooks:function(){this._draggable.off(this.getEvents(),this).disable(),L.DomUtil.removeClass(this._draggable._element,"leaflet-path-draggable")},moved:function(){return this._draggable&&this._draggable._moved},_onDragStart:function(){this._startPoint=this._draggable._startPoint,this._path.closePopup().fire("movestart").fire("dragstart")},_onDrag:function(t){var a=this._path,n=t.originalEvent.touches&&1===t.originalEvent.touches.length?t.originalEvent.touches[0]:t.originalEvent,i=L.point(n.clientX,n.clientY),e=a._map.layerPointToLatLng(i);this._offset=i.subtract(this._startPoint),this._startPoint=i,this._path.eachLatLng(this.updateLatLng,this),a.redraw(),t.latlng=e,t.offset=this._offset,a.fire("drag",t),t.latlng=this._path.getCenter?this._path.getCenter():this._path.getLatLng(),a.fire("move",t)},_onDragEnd:function(t){this._path._bounds&&this.resetBounds(),this._path.fire("moveend").fire("dragend",t)},latLngToLayerPoint:function(t){return this._path._map.project(L.latLng(t))._subtract(this._path._map.getPixelOrigin())},updateLatLng:function(t){var a=this.latLngToLayerPoint(t);a._add(this._offset);var n=this._path._map.layerPointToLatLng(a);t.lat=n.lat,t.lng=n.lng},resetBounds:function(){this._path._bounds=new L.LatLngBounds,this._path.eachLatLng(function(t){this._bounds.extend(t)})}}),L.Path.include({eachLatLng:function(t,a){a=a||this;var n=function(i){for(var e=0;e + + + + CC2 Editor + + +

Carrier Command 2 Save Editor (CC2E)

+ + + \ No newline at end of file diff --git a/cc2me/web/templates/map.html b/cc2me/web/templates/map.html new file mode 100644 index 0000000..57fa6c2 --- /dev/null +++ b/cc2me/web/templates/map.html @@ -0,0 +1,183 @@ + + + + + CC2 Map {{ filename }} - {{ displayname }} + + + + + + + + +
+ + + \ No newline at end of file diff --git a/cc2me/web/templates/slots.html b/cc2me/web/templates/slots.html new file mode 100644 index 0000000..4829244 --- /dev/null +++ b/cc2me/web/templates/slots.html @@ -0,0 +1,16 @@ + + + + + Save Slot Selection + + +

Select a slot to work on

+ + + + \ No newline at end of file diff --git a/cc2me/web/tool.py b/cc2me/web/tool.py new file mode 100644 index 0000000..a55581c --- /dev/null +++ b/cc2me/web/tool.py @@ -0,0 +1,13 @@ +"""Web server entrypoint for CC2 Editor""" + +import argparse +from typing import Optional, List +from . import server + +parser = argparse.ArgumentParser(description=__doc__) + + +def run(args: Optional[List[str]] = None) -> None: + parser.parse_args(args) + server.app.run("127.0.0.1", port=4422, debug=True) + diff --git a/cc2me/web/utils.py b/cc2me/web/utils.py new file mode 100644 index 0000000..c9324a1 --- /dev/null +++ b/cc2me/web/utils.py @@ -0,0 +1,57 @@ +from typing import Tuple, List, Dict, Any, Union +from dataclasses import dataclass +from ..savedata.types.utils import Location, Point3D + +SCALE_MAP_COORDS = 10000 + + +def cc2_to_minutes(num: float) -> float: + return num / SCALE_MAP_COORDS + + +def latlong_to_cc2loc(lat: float, lon: float) -> Location: + loc = Location() + loc.x = lon * SCALE_MAP_COORDS + loc.z = lat * SCALE_MAP_COORDS + return loc + + +@dataclass +class GPoint: + + lng: float + lat: float + + def move(self, dx: float = 0, dy: float = 0) -> "GPoint": + self.lng += dx + self.lat += dy + return self + + def as_list(self): + return [self.lat, self.lng] + + +def loc_to_geo(loc: Union[Location, Point3D]) -> GPoint: + return GPoint(loc.x / SCALE_MAP_COORDS, loc.z / SCALE_MAP_COORDS) + + +def loc_to_geo_box(loc: Location, w: float, h: float) -> List: + height = h / SCALE_MAP_COORDS + width = w / SCALE_MAP_COORDS + return [ + loc_to_geo(loc).move(dx=width / -2, dy=height / -2).as_list(), + loc_to_geo(loc).move(dx=width / 2, dy=height / -2).as_list(), + loc_to_geo(loc).move(dx=width / 2, dy=height / 2).as_list(), + loc_to_geo(loc).move(dx=width / -2, dy=height / 2).as_list(), + loc_to_geo(loc).move(dx=width / -2, dy=height / -2).as_list(), + ] + + +def make_geodata(**kwargs) -> Dict[str, Any]: + res = { + "type": "Feature", + "properties": {}, + } + for name, value in kwargs.items(): + res[name] = value + return res diff --git a/setup.py b/setup.py index b317339..42e46de 100644 --- a/setup.py +++ b/setup.py @@ -4,6 +4,8 @@ requirements = [ "tkintermapview==1.15", "pillow==9.2.0", + "pandas", + "flask", ] setup( @@ -25,7 +27,7 @@ "cc2me = cc2me.ui.tool:run" ], "console_scripts": [ - "cc2mec = cc2me.ui.tool:run" + "cce = cc2me.web.tool:run" ] }, )