summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsanine <sanine.not@pm.me>2022-06-05 20:38:18 -0500
committersanine <sanine.not@pm.me>2022-06-05 20:38:18 -0500
commitb3d9ffe8107bd7e0989c1fed2e5bdf31037134bb (patch)
tree6baaa464c8dd1e2b0e7cb4c31e897f6745edeb73
parentb9d4c658d3561106a452d8014c4d021fe175b759 (diff)
begin adding logical map shapes
-rw-r--r--notes.md62
-rw-r--r--src/Geometry/Geometry.js5
-rw-r--r--src/Map/Canvas.bak.js122
-rw-r--r--src/Map/Canvas.js132
-rw-r--r--src/Map/Map.js1
-rw-r--r--src/Map/Node.js15
-rw-r--r--src/Map/Shapes.js113
-rw-r--r--src/Map/World.js14
-rw-r--r--src/Util/Util.js11
-rw-r--r--src/index.html4
-rw-r--r--src/main.js26
11 files changed, 483 insertions, 22 deletions
diff --git a/notes.md b/notes.md
index 2a869db..d0d02c6 100644
--- a/notes.md
+++ b/notes.md
@@ -69,25 +69,55 @@ and those kinds of nodes should be on their own layers, so that the user can vie
ignore relevant data at any time.
-nodes
------
+points
+------
+
+Points are objects with the following properties **only**:
+
+ - x: the x-coordinate in meters
+ - y: the y-coordinate in meters
+
+
+shapes
+------
+
+A shape is an object containing the following properties **only**:
+
+ - type: a string, either 'point', 'path', or 'polygon'
+ - points: an array of `points`, or, if type is 'point', a single point object.
+ - boundingBox: AABB that contains every point in `points`
-node types:
- - point
- - path
- - polygon
-all nodes can be selected to view properties in the right pane. They can then be edited by clicking a button on the pane.
+mapObject
+---------
+
+mapObjects are objects with the following properties:
+
+ - core: a point
+ - shapes: an array of `shape` objects
+ - mapObjects: an array of child mapObjects
+ - boundingBox: AABB that contains the bounding boxes of every shape in shapes
+ - minZoom: a number from 0 to 6 (see maxZoom)
+ - maxZoom: a number from 0 to 7:
+ 0. global
+ 1. regional
+ 2. national
+ 3. provincial
+ 4. city
+ 5. neighborhood
+ 6. building
+
+
+tiles
+-----
+
+A tile is an object with the following properties:
-All nodes can be moved when editing. Path and polygon nodes can have their constituent points moved and can insert new nodes.
+ - region: an AABB bounding the tile area
+ - mapObjects: an array of references to the mapObjects that intersect this square
-Nodes have an associated "relevance level"
+worlds
+------
- - building (smallest)
- - neighborhood
- - city
- - province
- - nation
- - global (largest)
+a world is an object with the following properties:
-depending on a map's scale, some of these may not be relevant (i.e. a map only showing a city would not use higher relevance levels).
diff --git a/src/Geometry/Geometry.js b/src/Geometry/Geometry.js
index 6c296ad..110d764 100644
--- a/src/Geometry/Geometry.js
+++ b/src/Geometry/Geometry.js
@@ -152,6 +152,11 @@ class QTNode {
}
this.subnode = undefined;
}
+ /* no need to check if we have more than 1 leaf or 0 leaves --
+ * for more than 1 leaf we stay as a branch (so no need to do anything)
+ * and 0 leaves cannot happen, because this started as a branch (meaning 2+ leaves
+ * or at least one child branch) and we only removed 1 point from the tree.
+ */
break;
}
}
diff --git a/src/Map/Canvas.bak.js b/src/Map/Canvas.bak.js
new file mode 100644
index 0000000..efc6c00
--- /dev/null
+++ b/src/Map/Canvas.bak.js
@@ -0,0 +1,122 @@
+import h from '../Util/DomUtil.js';
+import { dist, AABB, QuadTree } from '../Geometry/Geometry.js';
+
+class Canvas {
+ constructor(parentId) {
+ const parentElement= document.getElementById(parentId);
+
+ this.canvas = h('canvas', parentElement.width, parentElement.height);
+ this.context = this.canvas.getContext('2d');
+
+ /* state */
+ this.movingPoint = false;
+ this.selectedPoint = null;
+
+ /* callbacks */
+ this.onMovePoint = (original, now) => console.log(original, now);
+
+ /* transform */
+ this.scale = 1;
+ this.pan = { x: 0, y: 0 };
+
+ /* mouse */
+ this.mouse = { x: 0, y: 0 };
+
+ /* retrieving points */
+ this.points = [];
+ this.tree = new QuadTree(this.canvas.width, this.canvas.height);
+
+ /* event listeners */
+ this.canvas.addEventListener('mousemove', e => {
+ this.mouse.x = e.offsetX;
+ this.mouse.y = e.offsetY;
+ if (this.movingPoint) this.render();
+ });
+
+ this.canvas.addEventListener('click', e => {
+ if (this.movingPoint) {
+ this.dropPoint();
+ }
+ else {
+ const pt = this.getClickedPoint(
+ { x: e.offsetX, y: e.offsetY }, 10
+ );
+ if (pt) this.grabPoint(pt);
+ }
+ });
+
+ /* attach to parent */
+ parentElement.appendChild(this.canvas);
+ }
+
+ /* transform a point to internal coordinates */
+ transform(x, y) {
+ return [
+ (this.scale * x) - this.pan.x,
+ (this.scale * y) - this.pan.y,
+ ];
+ }
+
+ /* data updates */
+ updatePoints(points) {
+ this.points = [];
+ this.tree = new QuadTree(this.canvas.width, this.canvas.height);
+ for (let pt of points) this.insertPoint(pt);
+ this.render();
+ }
+
+ insertPoint(pt) {
+ this.tree.insert(pt);
+ this.points.push(pt);
+ }
+
+ /* drawing */
+ drawPoint(pt) {
+ const ct = this.context;
+ ct.save();
+ ct.beginPath();
+ const [x, y] = this.transform(pt.x, pt.y);
+ console.log(x,y);
+ ct.arc(x, y, 10/this.scale, 0, 2*Math.PI);
+ ct.closePath();
+ ct.fill();
+ ct.restore();
+ }
+
+ render() {
+ this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
+ for (let pt of this.points) this.drawPoint(pt);
+ if (this.movingPoint) {
+ const [x, y] = this.transform(this.mouse.x, this.mouse.y);
+ this.drawPoint({x, y});
+ }
+ }
+
+ /* handling input */
+ grabPoint(pt) {
+ this.tree.root.remove(pt);
+ this.points = this.points.filter(point => (point.x !== pt.x || point.y !== pt.y));
+ console.log(this.points);
+ this.selectedPoint = pt;
+ this.movingPoint = true;
+ this.render();
+ }
+
+ dropPoint() {
+ const [x, y] = this.transform(this.mouse.x, this.mouse.y);
+ this.onMovePoint(this.selectedPoint, {x, y});
+ this.movingPoint = false;
+ this.selectedPoint = null;
+ }
+
+ getClickedPoint(clickLocation, maxDistance) {
+ const [ x, y ] = this.transform(clickLocation.x, clickLocation.y);
+ const clickPoint = { x, y };
+ const closest = this.tree.closest(clickPoint);
+ if (!closest) return null;
+ if (dist(closest, clickPoint) < maxDistance) return closest;
+ return null;
+ }
+}
+
+export default Canvas;
diff --git a/src/Map/Canvas.js b/src/Map/Canvas.js
new file mode 100644
index 0000000..cbd6e9e
--- /dev/null
+++ b/src/Map/Canvas.js
@@ -0,0 +1,132 @@
+import h from '../Util/DomUtil.js';
+import { dist, AABB, QuadTree } from '../Geometry/Geometry.js';
+import { Shape } from './Shapes.js';
+
+
+class Transform {
+ constructor() {
+ this.scale = 1;
+ this.pan = {
+ x: 0,
+ y: 0,
+ };
+ }
+
+ screenToDrawing(x, y) {
+ return [
+ (this.scale * x) - this.pan.x,
+ (this.scale * y) - this.pan.y,
+ ];
+ }
+
+ drawingToScreen(x, y) {
+ return [
+ (x + this.pan.x) / this.scale,
+ (y + this.pan.y) / this.scale,
+ ];
+ }
+}
+
+
+class Mouse {
+ constructor(canvas) {
+ this.position = { x: 0, y: 0 };
+
+ this.onMouseMove = null;
+ this.onClick = null;
+
+ canvas.addEventListener('mousemove', e => {
+ this.position.x = e.offsetX;
+ this.position.y = e.offsetY;
+ if (this.onMouseMove) this.onMouseMove();
+ });
+
+ canvas.addEventListener('click', e => {
+ if (this.onClick) this.onClick();
+ });
+ }
+
+ getPosition(transform) {
+ const { x, y } = this.position;
+ return transform.screenToDrawing(x, y);
+ }
+}
+
+
+class Canvas {
+ constructor(parentId) {
+ const parentElement = document.getElementById(parentId);
+ this.canvas = h('canvas',
+ {
+ width: parentElement.clientWidth,
+ height: parentElement.clientHeight
+ }
+ );
+ this.context = this.canvas.getContext('2d');
+
+ parentElement.appendChild(this.canvas);
+ }
+
+ /* ~~~~~~~~~~~~~~~~ drawing ~~~~~~~~~~~~~~~~ */
+
+ render(transform, shapes) {
+ this.context.save();
+ this.context.setTransform(1, 0, 0, 1, 0, 0);
+ this.context.scale(transform.scale, transform.scale);
+ this.context.translate(transform.pan.x, transform.pan.y);
+
+ for (let shape of shapes) {
+ switch(shape.type) {
+ case Shape.Point:
+ this.drawPoint(shape);
+ break;
+
+ case Shape.Path:
+ this.drawPath(shape);
+ break;
+
+ case Shape.Polygon:
+ this.drawPolygon(shape);
+ break;
+
+ default:
+ break;
+ }
+ }
+ this.context.restore();
+ }
+
+ drawPoint(shape) {
+ const ct = this.context;
+ ct.beginPath();
+ ct.arc(shape.nodes[0].x, shape.nodes[0].y, 2, 0, 2*Math.PI);
+ ct.closePath();
+ ct.fill();
+ }
+
+ drawPath(shape) {
+ const ct = this.context;
+ ct.beginPath();
+ ct.moveTo(shape.nodes[0].x, shape.nodes[0].y);
+ for(let point of shape.nodes.slice(1)) {
+ ct.lineTo(point.x, point.y);
+ }
+ ct.stroke();
+ ct.closePath();
+ }
+
+ drawPolygon(shape) {
+ const ct = this.context;
+ ct.beginPath();
+ ct.moveTo(shape.nodes[0].x, shape.nodes[0].y);
+ for (let point of shape.nodes.slice(1)) {
+ ct.lineTo(point.x, point.y);
+ }
+ ct.closePath();
+ ct.stroke();
+ ct.fill();
+ }
+}
+
+
+export { Transform, Mouse, Canvas };
diff --git a/src/Map/Map.js b/src/Map/Map.js
deleted file mode 100644
index 8b13789..0000000
--- a/src/Map/Map.js
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/src/Map/Node.js b/src/Map/Node.js
new file mode 100644
index 0000000..12308ca
--- /dev/null
+++ b/src/Map/Node.js
@@ -0,0 +1,15 @@
+class PointNode {
+ constructor(x, y, attributes) {
+ this.setPosition(x, y);
+ this.attributes = attributes;
+ }
+
+ setPosition(x, y) {
+ this.x = x;
+ this.y = y;
+ }
+
+ setAttribute(key, value) {
+ this.attributes[key] = value;
+ }
+}
diff --git a/src/Map/Shapes.js b/src/Map/Shapes.js
new file mode 100644
index 0000000..3443f73
--- /dev/null
+++ b/src/Map/Shapes.js
@@ -0,0 +1,113 @@
+import { Enum } from '../Util/Util.js';
+import { dist, AABB } from '../Geometry/Geometry.js';
+
+
+class Node {
+ constructor(x, y) {
+ this.moveTo(x, y);
+ }
+
+ moveTo(x, y) {
+ this.x = x;
+ this.y = y;
+ }
+}
+
+
+class ShapeType extends Enum {
+ constructor(name) { super('Shape', name); }
+}
+
+
+class Shape {
+ static Point = new ShapeType('Point');
+ static Path = new ShapeType('Path');
+ static Polygon = new ShapeType('Polygon');
+
+ constructor(type, nodes) {
+ this.type = type;
+ this.nodes = nodes;
+
+ this.boundingBox = this.computeBoundingBox();
+ }
+
+ computeBoundingBox() {
+ let xMin = Infinity;
+ let xMax = -Infinity;
+ let yMin = Infinity;
+ let yMax = -Infinity;
+
+ for (let node of this.nodes) {
+ if (node.x < xMin) xMin = node.x;
+ if (node.x > xMax) xMax = node.x;
+
+ if (node.y < yMin) yMin = node.y;
+ if (node.y > yMax) yMax = node.y;
+ }
+
+ return new AABB(xMin, yMin, xMax-xMin, yMax-yMin);
+ }
+}
+
+class Point extends Shape {
+ constructor(node) {
+ super(Shape.Point, [node]);
+ }
+}
+
+class Path extends Shape {
+ constructor(nodes) {
+ super(Shape.Path, nodes);
+ }
+}
+
+class Polygon extends Shape {
+ constructor(nodes) {
+ super(Shape.Polygon, nodes);
+ }
+}
+
+
+class ZoomLevel extends Enum {
+ static Globe = new ZoomLevel('Globe');
+ static Nation = new ZoomLevel('Nation');
+ static Province = new ZoomLevel('Province');
+ static City = new ZoomLevel('City');
+ static Neighborhoot = new ZoomLevel('Neighborhood');
+ static Building = new ZoomLevel('Building');
+
+ constructor(name) { super('ZoomLevel', name); }
+}
+
+class MapObject {
+ constructor(core, layerId, minZoom, maxZoom=minZoom) {
+ this.core = core;
+ this.layerId = layerId;
+ this.minZoom = minZoom;
+ this.maxZoom = maxZoom;
+
+ this.shapes = [];
+ this.boundingBox = this.computeBoundingBox();
+ }
+
+ computeBoundingBox() {
+ let xMin = Infinity;
+ let xMax = -Infinity;
+ let yMin = Infinity;
+ let yMax = -Infinity;
+
+ for (let shape of this.shapes) {
+ const box = shape.boundingBox;
+ if (box.x < xMin) xMin = box.x;
+ if (box.x + box.width > xMax) xMax = box.x + box.width;
+
+ if (box.y < yMin) yMin = box.y;
+ if (box.y + box.height > yMax) yMax = box.y + box.height;
+ }
+
+ return new AABB(xMin, yMin, xMax-xMin, yMax-yMin);
+ }
+}
+
+
+export { Node, Shape, Point, Path, Polygon, ZoomLevel, MapObject };
diff --git a/src/Map/World.js b/src/Map/World.js
new file mode 100644
index 0000000..b9db4e9
--- /dev/null
+++ b/src/Map/World.js
@@ -0,0 +1,14 @@
+import { Shape, ZoomLevel } from './Shape.js';
+
+
+class Tile {
+
+}
+
+
+class World {
+
+}
+
+
+export default World;
diff --git a/src/Util/Util.js b/src/Util/Util.js
index 165d1d0..88ed59f 100644
--- a/src/Util/Util.js
+++ b/src/Util/Util.js
@@ -17,4 +17,13 @@ function clamp(value, min, max) {
function lerp(a, b, alpha) {
return ((1-alpha)*a) + (alpha*b);
}
-export { useAverage, clamp, lerp };
+
+class Enum {
+ constructor(prefix, name) {
+ this.prefix = prefix;
+ this.name = name;
+ }
+ toString() { return `${this.prefix}.${this.name}`; }
+}
+
+export { useAverage, clamp, lerp, Enum };
diff --git a/src/index.html b/src/index.html
index 55e00b1..9ff8fd3 100644
--- a/src/index.html
+++ b/src/index.html
@@ -11,6 +11,10 @@
background-color: #222;
color: white;
}
+ #root {
+ width: 500px;
+ height: 500px;
+ }
button {
font-family: monospace;
}
diff --git a/src/main.js b/src/main.js
index 0474d7a..5c6db59 100644
--- a/src/main.js
+++ b/src/main.js
@@ -1,5 +1,23 @@
-import { LayerData, LayerView, LayerController } from './Map/Layer.js';
+import { Transform, Canvas } from './Map/Canvas.js';
+import { Node, Shape, Point, Path, Polygon } from './Map/Shapes.js';
-const view = new LayerView('root');
-const data = new LayerData();
-const con = new LayerController(data, view);
+function point(x, y) { this.x = x; this.y = y; };
+
+const canvas = new Canvas('root');
+const transform = new Transform();
+
+const shapes = [
+ new Point(new Node(30, 30)),
+ new Path([
+ new Node(100, 100),
+ new Node(100, 200),
+ new Node(150, 250),
+ ]),
+ new Polygon([
+ new Node(200, 100),
+ new Node(200, 200),
+ new Node(300, 200),
+ ]),
+];
+
+canvas.render(transform, shapes);