From b3d9ffe8107bd7e0989c1fed2e5bdf31037134bb Mon Sep 17 00:00:00 2001 From: sanine Date: Sun, 5 Jun 2022 20:38:18 -0500 Subject: begin adding logical map shapes --- notes.md | 62 ++++++++++++++++------ src/Geometry/Geometry.js | 5 ++ src/Map/Canvas.bak.js | 122 +++++++++++++++++++++++++++++++++++++++++++ src/Map/Canvas.js | 132 +++++++++++++++++++++++++++++++++++++++++++++++ src/Map/Map.js | 1 - src/Map/Node.js | 15 ++++++ src/Map/Shapes.js | 113 ++++++++++++++++++++++++++++++++++++++++ src/Map/World.js | 14 +++++ src/Util/Util.js | 11 +++- src/index.html | 4 ++ src/main.js | 26 ++++++++-- 11 files changed, 483 insertions(+), 22 deletions(-) create mode 100644 src/Map/Canvas.bak.js create mode 100644 src/Map/Canvas.js delete mode 100644 src/Map/Map.js create mode 100644 src/Map/Node.js create mode 100644 src/Map/Shapes.js create mode 100644 src/Map/World.js 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); -- cgit v1.2.1