Started readding old core frontend code

This commit is contained in:
Pax1601
2024-04-03 08:05:36 +02:00
parent 5d5c650162
commit d72a00a005
60 changed files with 16018 additions and 34 deletions

View File

@@ -0,0 +1,136 @@
import { Map } from 'leaflet';
import { Handler } from 'leaflet';
import { Util } from 'leaflet';
import { DomUtil } from 'leaflet';
import { DomEvent } from 'leaflet';
import { LatLngBounds } from 'leaflet';
import { Bounds } from 'leaflet';
export var BoxSelect = Handler.extend({
initialize: function (map) {
this._map = map;
this._container = map.getContainer();
this._pane = map.getPanes().overlayPane;
this._resetStateTimeout = 0;
map.on('unload', this._destroy, this);
},
addHooks: function () {
DomEvent.on(this._container, 'mousedown', this._onMouseDown, this);
},
removeHooks: function () {
DomEvent.off(this._container, 'mousedown', this._onMouseDown, this);
},
moved: function () {
return this._moved;
},
_destroy: function () {
DomUtil.remove(this._pane);
delete this._pane;
},
_resetState: function () {
this._resetStateTimeout = 0;
this._moved = false;
},
_clearDeferredResetState: function () {
if (this._resetStateTimeout !== 0) {
clearTimeout(this._resetStateTimeout);
this._resetStateTimeout = 0;
}
},
_onMouseDown: function (e: any) {
if ((e.which == 1 && e.button == 0 && e.shiftKey)) {
this._map.fire('selectionstart');
// Clear the deferred resetState if it hasn't executed yet, otherwise it
// will interrupt the interaction and orphan a box element in the container.
this._clearDeferredResetState();
this._resetState();
DomUtil.disableTextSelection();
DomUtil.disableImageDrag();
this._startPoint = this._map.mouseEventToContainerPoint(e);
//@ts-ignore
DomEvent.on(document, {
contextmenu: DomEvent.stop,
mousemove: this._onMouseMove,
mouseup: this._onMouseUp,
keydown: this._onKeyDown
}, this);
} else {
return false;
}
},
_onMouseMove: function (e: any) {
if (!this._moved) {
this._moved = true;
this._box = DomUtil.create('div', 'leaflet-zoom-box', this._container);
DomUtil.addClass(this._container, 'leaflet-crosshair');
this._map.fire('boxzoomstart');
}
this._point = this._map.mouseEventToContainerPoint(e);
var bounds = new Bounds(this._point, this._startPoint),
size = bounds.getSize();
if (bounds.min != undefined)
DomUtil.setPosition(this._box, bounds.min);
this._box.style.width = size.x + 'px';
this._box.style.height = size.y + 'px';
},
_finish: function () {
if (this._moved) {
DomUtil.remove(this._box);
DomUtil.removeClass(this._container, 'leaflet-crosshair');
}
DomUtil.enableTextSelection();
DomUtil.enableImageDrag();
//@ts-ignore
DomEvent.off(document, {
contextmenu: DomEvent.stop,
mousemove: this._onMouseMove,
mouseup: this._onMouseUp,
keydown: this._onKeyDown
}, this);
},
_onMouseUp: function (e: any) {
if ((e.which !== 1) && (e.button !== 0)) { return; }
this._finish();
if (!this._moved) { return; }
// Postpone to next JS tick so internal click event handling
// still see it as "moved".
window.setTimeout(Util.bind(this._resetState, this), 0);
var bounds = new LatLngBounds(
this._map.containerPointToLatLng(this._startPoint),
this._map.containerPointToLatLng(this._point));
this._map.fire('selectionend', { selectionBounds: bounds });
},
_onKeyDown: function (e: any) {
if (e.keyCode === 27) {
this._finish();
this._clearDeferredResetState();
this._resetState();
}
}
});

View File

@@ -0,0 +1,12 @@
import { MiniMap, MiniMapOptions } from "leaflet-control-mini-map";
export class ClickableMiniMap extends MiniMap {
constructor(layer: L.TileLayer | L.LayerGroup, options?: MiniMapOptions) {
super(layer, options);
}
getMap() {
//@ts-ignore needed to access not exported member. A bit of a hack, required to access click events //TODO: fix me
return this._miniMap;
}
}

View File

@@ -0,0 +1,166 @@
import { DomUtil, LatLng, LatLngExpression, Map, Point, Polygon, PolylineOptions } from "leaflet";
import { getApp } from "../../olympusapp";
import { CoalitionAreaHandle } from "./coalitionareahandle";
import { CoalitionAreaMiddleHandle } from "./coalitionareamiddlehandle";
import { BLUE_COMMANDER, RED_COMMANDER } from "../../constants/constants";
export class CoalitionArea extends Polygon {
#coalition: string = "blue";
#selected: boolean = true;
#editing: boolean = true;
#handles: CoalitionAreaHandle[] = [];
#middleHandles: CoalitionAreaMiddleHandle[] = [];
#activeIndex: number = 0;
constructor(latlngs: LatLngExpression[] | LatLngExpression[][] | LatLngExpression[][][], options?: PolylineOptions) {
if (options === undefined)
options = {};
options.bubblingMouseEvents = false;
options.interactive = false;
super(latlngs, options);
this.#setColors();
this.#registerCallbacks();
if ([BLUE_COMMANDER, RED_COMMANDER].includes(getApp().getMissionManager().getCommandModeOptions().commandMode))
this.setCoalition(getApp().getMissionManager().getCommandedCoalition());
}
setCoalition(coalition: string) {
this.#coalition = coalition;
this.#setColors();
}
getCoalition() {
return this.#coalition;
}
setSelected(selected: boolean) {
this.#selected = selected;
this.#setColors();
this.#setHandles();
this.setOpacity(selected? 1: 0.5);
if (!this.getSelected() && this.getEditing()) {
/* Remove the vertex we were working on */
var latlngs = this.getLatLngs()[0] as LatLng[];
latlngs.splice(this.#activeIndex, 1);
this.setLatLngs(latlngs);
this.setEditing(false);
}
}
getSelected() {
return this.#selected;
}
setEditing(editing: boolean) {
this.#editing = editing;
this.#setHandles();
var latlngs = this.getLatLngs()[0] as LatLng[];
/* Remove areas with less than 2 vertexes */
if (latlngs.length <= 2)
getApp().getMap().deleteCoalitionArea(this);
}
getEditing() {
return this.#editing;
}
addTemporaryLatLng(latlng: LatLng) {
this.#activeIndex++;
var latlngs = this.getLatLngs()[0] as LatLng[];
latlngs.splice(this.#activeIndex, 0, latlng);
this.setLatLngs(latlngs);
this.#setHandles();
}
moveActiveVertex(latlng: LatLng) {
var latlngs = this.getLatLngs()[0] as LatLng[];
latlngs[this.#activeIndex] = latlng;
this.setLatLngs(latlngs);
this.#setHandles();
}
setOpacity(opacity: number) {
this.setStyle({opacity: opacity, fillOpacity: opacity * 0.25});
}
onRemove(map: Map): this {
super.onRemove(map);
this.#handles.concat(this.#middleHandles).forEach((handle: CoalitionAreaHandle | CoalitionAreaMiddleHandle) => handle.removeFrom(getApp().getMap()));
return this;
}
#setColors() {
const coalitionColor = this.getCoalition() === "blue" ? "#247be2" : "#ff5858";
this.setStyle({ color: this.getSelected() ? "white" : coalitionColor, fillColor: coalitionColor });
}
#setHandles() {
this.#handles.forEach((handle: CoalitionAreaHandle) => handle.removeFrom(getApp().getMap()));
this.#handles = [];
if (this.getSelected()) {
var latlngs = this.getLatLngs()[0] as LatLng[];
latlngs.forEach((latlng: LatLng, idx: number) => {
/* Add the polygon vertex handle (for moving the vertex) */
const handle = new CoalitionAreaHandle(latlng);
handle.addTo(getApp().getMap());
handle.on("drag", (e: any) => {
var latlngs = this.getLatLngs()[0] as LatLng[];
latlngs[idx] = e.target.getLatLng();
this.setLatLngs(latlngs);
this.#setMiddleHandles();
});
this.#handles.push(handle);
});
}
this.#setMiddleHandles();
}
#setMiddleHandles() {
this.#middleHandles.forEach((handle: CoalitionAreaMiddleHandle) => handle.removeFrom(getApp().getMap()));
this.#middleHandles = [];
var latlngs = this.getLatLngs()[0] as LatLng[];
if (this.getSelected() && latlngs.length >= 2) {
var lastLatLng: LatLng | null = null;
latlngs.concat([latlngs[0]]).forEach((latlng: LatLng, idx: number) => {
/* Add the polygon middle point handle (for adding new vertexes) */
if (lastLatLng != null) {
const handle1Point = getApp().getMap().latLngToLayerPoint(latlng);
const handle2Point = getApp().getMap().latLngToLayerPoint(lastLatLng);
const middlePoint = new Point((handle1Point.x + handle2Point.x) / 2, (handle1Point.y + handle2Point.y) / 2);
const middleLatLng = getApp().getMap().layerPointToLatLng(middlePoint);
const middleHandle = new CoalitionAreaMiddleHandle(middleLatLng);
middleHandle.addTo(getApp().getMap());
middleHandle.on("click", (e: any) => {
this.#activeIndex = idx - 1;
this.addTemporaryLatLng(middleLatLng);
});
this.#middleHandles.push(middleHandle);
}
lastLatLng = latlng;
});
}
}
#registerCallbacks() {
this.on("click", (e: any) => {
getApp().getMap().deselectAllCoalitionAreas();
if (!this.getSelected()) {
this.setSelected(true);
}
});
this.on("contextmenu", (e: any) => {
if (!this.getEditing()) {
getApp().getMap().deselectAllCoalitionAreas();
this.setSelected(true);
}
else
this.setEditing(false);
});
}
}

View File

@@ -0,0 +1,19 @@
import { DivIcon, LatLng } from "leaflet";
import { CustomMarker } from "../markers/custommarker";
export class CoalitionAreaHandle extends CustomMarker {
constructor(latlng: LatLng) {
super(latlng, {interactive: true, draggable: true});
}
createIcon() {
this.setIcon(new DivIcon({
iconSize: [24, 24],
iconAnchor: [12, 12],
className: "leaflet-coalitionarea-handle-marker",
}));
var el = document.createElement("div");
el.classList.add("ol-coalitionarea-handle-icon");
this.getElement()?.appendChild(el);
}
}

View File

@@ -0,0 +1,19 @@
import { DivIcon, LatLng } from "leaflet";
import { CustomMarker } from "../markers/custommarker";
export class CoalitionAreaMiddleHandle extends CustomMarker {
constructor(latlng: LatLng) {
super(latlng, {interactive: true, draggable: false});
}
createIcon() {
this.setIcon(new DivIcon({
iconSize: [16, 16],
iconAnchor: [8, 8],
className: "leaflet-coalitionarea-middle-handle-marker",
}));
var el = document.createElement("div");
el.classList.add("ol-coalitionarea-middle-handle-icon");
this.getElement()?.appendChild(el);
}
}

View File

@@ -0,0 +1,20 @@
import { DivIcon, LatLng } from "leaflet";
import { CustomMarker } from "../markers/custommarker";
export class DrawingCursor extends CustomMarker {
constructor() {
super(new LatLng(0, 0), {interactive: false})
this.setZIndexOffset(9999);
}
createIcon() {
this.setIcon(new DivIcon({
iconSize: [24, 24],
iconAnchor: [0, 24],
className: "leaflet-draw-marker",
}));
var el = document.createElement("div");
el.classList.add("ol-draw-icon");
this.getElement()?.appendChild(el);
}
}

View File

@@ -0,0 +1,49 @@
import * as L from "leaflet"
export class DCSLayer extends L.TileLayer {
createTile(coords: L.Coords, done: L.DoneCallback) {
let newDone = (error?: Error, tile?: HTMLElement) => {
if (error === null && tile !== undefined && !tile.classList.contains('filtered')) {
// Create a canvas and set its width and height.
var canvas = document.createElement('canvas');
canvas.setAttribute('width', '256px');
canvas.setAttribute('height', '256px');
// Get the canvas drawing context, and draw the image to it.
var context = canvas.getContext('2d');
if (context) {
context.drawImage(tile as CanvasImageSource, 0, 0, canvas.width, canvas.height);
// Get the canvas image data.
var imageData = context.getImageData(0, 0, canvas.width, canvas.height);
// Create a function for preserving a specified colour.
var makeTransparent = function(imageData: ImageData, color: {r: number, g: number, b: number}) {
// Get the pixel data from the source.
var data = imageData.data;
// Iterate through all the pixels.
for (var i = 0; i < data.length; i += 4) {
// Check if the current pixel should have preserved transparency. This simply compares whether the color we passed in is equivalent to our pixel data.
var convert = data[i] > color.r - 5 && data[i] < color.r + 5
&& data[i + 1] > color.g - 5 && data[i + 1] < color.g + 5
&& data[i + 2] > color.b - 5 && data[i + 2] < color.b + 5;
// Either preserve the initial transparency or set the transparency to 0.
data[i + 3] = convert ? 100: data[i + 3];
}
return imageData;
};
// Get the new pixel data and set it to the canvas context.
var newData = makeTransparent(imageData, {r: 26, g: 109, b: 127});
context.putImageData(newData, 0, 0);
(tile as HTMLImageElement).src = canvas.toDataURL();
tile.classList.add('filtered');
}
} else {
return done(error, tile);
}
}
return super.createTile(coords, newDone);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,25 @@
import { DivIcon, Map, Marker } from "leaflet";
import { MarkerOptions } from "leaflet";
import { LatLngExpression } from "leaflet";
export class CustomMarker extends Marker {
constructor(latlng: LatLngExpression, options?: MarkerOptions) {
super(latlng, options);
}
onAdd(map: Map): this {
this.setIcon(new DivIcon()); // Default empty icon
super.onAdd(map);
this.createIcon();
return this;
}
onRemove(map: Map): this {
super.onRemove(map);
return this;
}
createIcon() {
/* Overloaded by child classes */
}
}

View File

@@ -0,0 +1,19 @@
import { DivIcon, LatLng } from "leaflet";
import { CustomMarker } from "../markers/custommarker";
export class DestinationPreviewHandle extends CustomMarker {
constructor(latlng: LatLng) {
super(latlng, {interactive: true, draggable: true});
}
createIcon() {
this.setIcon(new DivIcon({
iconSize: [18, 18],
iconAnchor: [9, 9],
className: "leaflet-destination-preview-handle-marker",
}));
var el = document.createElement("div");
el.classList.add("ol-destination-preview-handle-icon");
this.getElement()?.appendChild(el);
}
}

View File

@@ -0,0 +1,20 @@
import { DivIcon, LatLngExpression, MarkerOptions } from "leaflet";
import { CustomMarker } from "./custommarker";
export class DestinationPreviewMarker extends CustomMarker {
constructor(latlng: LatLngExpression, options?: MarkerOptions) {
super(latlng, options);
this.setZIndexOffset(9999);
}
createIcon() {
this.setIcon(new DivIcon({
iconSize: [52, 52],
iconAnchor: [26, 26],
className: "leaflet-destination-preview",
}));
var el = document.createElement("div");
el.classList.add("ol-destination-preview-icon");
this.getElement()?.appendChild(el);
}
}

View File

@@ -0,0 +1,31 @@
import { DivIcon, LatLngExpression, MarkerOptions } from "leaflet";
import { CustomMarker } from "./custommarker";
import { SVGInjector } from "@tanem/svg-injector";
import { getApp } from "../../olympusapp";
export class SmokeMarker extends CustomMarker {
#color: string;
constructor(latlng: LatLngExpression, color: string, options?: MarkerOptions) {
super(latlng, options);
this.setZIndexOffset(9999);
this.#color = color;
window.setTimeout(() => { this.removeFrom(getApp().getMap()); }, 300000) /* Remove the smoke after 5 minutes */
}
createIcon() {
this.setIcon(new DivIcon({
iconSize: [52, 52],
iconAnchor: [26, 52],
className: "leaflet-smoke-marker",
}));
var el = document.createElement("div");
el.classList.add("ol-smoke-icon");
el.setAttribute("data-color", this.#color);
var img = document.createElement("img");
img.src = "/resources/theme/images/markers/smoke.svg";
img.onload = () => SVGInjector(img);
el.appendChild(img);
this.getElement()?.appendChild(el);
}
}

View File

@@ -0,0 +1,20 @@
import { DivIcon, LatLngExpression, MarkerOptions } from "leaflet";
import { CustomMarker } from "./custommarker";
export class TargetMarker extends CustomMarker {
constructor(latlng: LatLngExpression, options?: MarkerOptions) {
super(latlng, options);
this.setZIndexOffset(9999);
}
createIcon() {
this.setIcon(new DivIcon({
iconSize: [52, 52],
iconAnchor: [26, 26],
className: "leaflet-target-marker",
}));
var el = document.createElement("div");
el.classList.add("ol-target-icon");
this.getElement()?.appendChild(el);
}
}

View File

@@ -0,0 +1,76 @@
import { CustomMarker } from "./custommarker";
import { DivIcon, LatLng } from "leaflet";
import { SVGInjector } from "@tanem/svg-injector";
import { getMarkerCategoryByName, getUnitDatabaseByCategory } from "../../other/utils";
import { getApp } from "../../olympusapp";
export class TemporaryUnitMarker extends CustomMarker {
#name: string;
#coalition: string;
#commandHash: string|undefined = undefined;
#timer: number = 0;
constructor(latlng: LatLng, name: string, coalition: string, commandHash?: string) {
super(latlng, {interactive: false});
this.#name = name;
this.#coalition = coalition;
this.#commandHash = commandHash;
if (commandHash !== undefined)
this.setCommandHash(commandHash)
}
setCommandHash(commandHash: string) {
this.#commandHash = commandHash;
this.#timer = window.setInterval(() => {
if (this.#commandHash !== undefined) {
getApp().getServerManager().isCommandExecuted((res: any) => {
if (res.commandExecuted) {
this.removeFrom(getApp().getMap());
window.clearInterval(this.#timer);
}
}, this.#commandHash)
}
}, 1000);
}
createIcon() {
const category = getMarkerCategoryByName(this.#name);
const databaseEntry = getUnitDatabaseByCategory(category)?.getByName(this.#name);
/* Set the icon */
var icon = new DivIcon({
className: 'leaflet-unit-icon',
iconAnchor: [25, 25],
iconSize: [50, 50],
});
this.setIcon(icon);
var el = document.createElement("div");
el.classList.add("unit");
el.setAttribute("data-object", `unit-${category}`);
el.setAttribute("data-coalition", this.#coalition);
// Main icon
var unitIcon = document.createElement("div");
unitIcon.classList.add("unit-icon");
var img = document.createElement("img");
img.src = `/resources/theme/images/units/${databaseEntry?.markerFile ?? category}.svg`;
img.onload = () => SVGInjector(img);
unitIcon.appendChild(img);
unitIcon.toggleAttribute("data-rotate-to-heading", false);
el.append(unitIcon);
// Short label
if (category == "aircraft" || category == "helicopter") {
var shortLabel = document.createElement("div");
shortLabel.classList.add("unit-short-label");
shortLabel.innerText = databaseEntry?.shortLabel || "";
el.append(shortLabel);
}
this.getElement()?.appendChild(el);
this.getElement()?.classList.add("ol-temporary-marker");
}
}

View File

@@ -0,0 +1,56 @@
// @ts-nocheck
// This is a horrible hack. But it is needed at the moment to ovveride a default behaviour of Leaflet. TODO please fix me the proper way.
import { Circle, Point, Polyline } from 'leaflet';
/**
* This custom Circle object implements a faster render method for very big circles. When zoomed in, the default ctx.arc method
* is very slow since the circle is huge. Also, when zoomed in most of the circle points will be outside the screen and not needed. This
* simpler, faster renderer approximates the circle with line segements and only draws those currently visibile.
* A more refined version using arcs could be implemented but this works good enough.
*/
export class RangeCircle extends Circle {
_updatePath() {
if (!this._renderer._drawing || this._empty()) { return; }
var p = this._point,
ctx = this._renderer._ctx,
r = Math.max(Math.round(this._radius), 1),
s = (Math.max(Math.round(this._radiusY), 1) || r) / r;
if (s !== 1) {
ctx.save();
ctx.scale(1, s);
}
let pathBegun = false;
let dtheta = Math.PI * 2 / 120;
for (let theta = 0; theta <= Math.PI * 2; theta += dtheta) {
let p1 = new Point(p.x + r * Math.cos(theta), p.y / s + r * Math.sin(theta));
let p2 = new Point(p.x + r * Math.cos(theta + dtheta), p.y / s + r * Math.sin(theta + dtheta));
let l1 = this._map.layerPointToLatLng(p1);
let l2 = this._map.layerPointToLatLng(p2);
let line = new Polyline([l1, l2]);
if (this._map.getBounds().intersects(line.getBounds())) {
if (!pathBegun) {
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
pathBegun = true;
}
ctx.lineTo(p2.x, p2.y);
}
else {
if (pathBegun) {
this._renderer._fillStroke(ctx, this);
pathBegun = false;
}
}
}
if (pathBegun)
this._renderer._fillStroke(ctx, this);
if (s !== 1)
ctx.restore();
}
}

View File

@@ -0,0 +1,136 @@
import { Map, Point } from 'leaflet';
import { Handler } from 'leaflet';
import { Util } from 'leaflet';
import { DomUtil } from 'leaflet';
import { DomEvent } from 'leaflet';
import { LatLngBounds } from 'leaflet';
import { Bounds } from 'leaflet';
export var TouchBoxSelect = Handler.extend({
initialize: function (map: Map) {
this._map = map;
this._container = map.getContainer();
this._pane = map.getPanes().overlayPane;
this._resetStateTimeout = 0;
this._doubleClicked = false;
map.on('unload', this._destroy, this);
},
addHooks: function () {
DomEvent.on(this._container, 'touchstart', this._onMouseDown, this);
},
removeHooks: function () {
DomEvent.off(this._container, 'touchstart', this._onMouseDown, this);
},
moved: function () {
return this._moved;
},
_destroy: function () {
DomUtil.remove(this._pane);
delete this._pane;
},
_resetState: function () {
this._resetStateTimeout = 0;
this._moved = false;
},
_clearDeferredResetState: function () {
if (this._resetStateTimeout !== 0) {
clearTimeout(this._resetStateTimeout);
this._resetStateTimeout = 0;
}
},
_onMouseDown: function (e: any) {
if ((e.which == 0)) {
this._map.fire('selectionstart');
// Clear the deferred resetState if it hasn't executed yet, otherwise it
// will interrupt the interaction and orphan a box element in the container.
this._clearDeferredResetState();
this._resetState();
DomUtil.disableTextSelection();
DomUtil.disableImageDrag();
this._startPoint = this._getMousePosition(e);
//@ts-ignore
DomEvent.on(document, {
contextmenu: DomEvent.stop,
touchmove: this._onMouseMove,
touchend: this._onMouseUp
}, this);
} else {
return false;
}
},
_onMouseMove: function (e: any) {
if (!this._moved) {
this._moved = true;
this._box = DomUtil.create('div', 'leaflet-zoom-box', this._container);
DomUtil.addClass(this._container, 'leaflet-crosshair');
}
this._point = this._getMousePosition(e);
var bounds = new Bounds(this._point, this._startPoint),
size = bounds.getSize();
if (bounds.min != undefined)
DomUtil.setPosition(this._box, bounds.min);
this._box.style.width = size.x + 'px';
this._box.style.height = size.y + 'px';
},
_finish: function () {
if (this._moved) {
DomUtil.remove(this._box);
DomUtil.removeClass(this._container, 'leaflet-crosshair');
}
DomUtil.enableTextSelection();
DomUtil.enableImageDrag();
//@ts-ignore
DomEvent.off(document, {
contextmenu: DomEvent.stop,
touchmove: this._onMouseMove,
touchend: this._onMouseUp
}, this);
},
_onMouseUp: function (e: any) {
if ((e.which !== 0)) { return; }
this._finish();
if (!this._moved) { return; }
// Postpone to next JS tick so internal click event handling
// still see it as "moved".
window.setTimeout(Util.bind(this._resetState, this), 0);
var bounds = new LatLngBounds(
this._map.containerPointToLatLng(this._startPoint),
this._map.containerPointToLatLng(this._point));
this._map.fire('selectionend', { selectionBounds: bounds });
},
_getMousePosition(e: any) {
var scale = DomUtil.getScale(this._container), offset = scale.boundingClientRect; // left and top values are in page scale (like the event clientX/Y)
return new Point(
// offset.left/top values are in page scale (like clientX/Y),
// whereas clientLeft/Top (border width) values are the original values (before CSS scale applies).
(e.touches[0].clientX - offset.left) / scale.x - this._container.clientLeft,
(e.touches[0].clientY - offset.top) / scale.y - this._container.clientTop
);
}
});