lightning-maps
Advanced tools
Comparing version 0.0.6 to 0.0.7
@@ -9,3 +9,4 @@ { | ||
"test": "mocha --require babel-register --colors ./test/*.spec.js", | ||
"test:watch": "mocha --require babel-register --colors -w ./test/*.spec.js" | ||
"test:watch": "mocha --require babel-register --colors -w ./test/*.spec.js", | ||
"bump": "npx standard-version" | ||
}, | ||
@@ -30,10 +31,7 @@ "repository": { | ||
"devDependencies": { | ||
"@babel/cli": "^7.0.0-beta.51", | ||
"@babel/core": "^7.0.0-beta.51", | ||
"@babel/preset-env": "^7.0.0-beta.51", | ||
"babel-eslint": "^8.0.3", | ||
"babel-loader": "^8.0.0-beta.4", | ||
"babel-plugin-add-module-exports": "^0.2.1", | ||
"babel-preset-env": "^7.0.0-beta.3", | ||
"babel-register": "^7.0.0-beta.3", | ||
"@babel/cli": "^7.2.3", | ||
"@babel/core": "^7.2.2", | ||
"@babel/preset-env": "^7.3.1", | ||
"babel-eslint": "^10.0.1", | ||
"babel-loader": "^8.0.5", | ||
"chai": "^4.1.2", | ||
@@ -45,2 +43,3 @@ "eslint": "^5.0.1", | ||
"mocha": "^4.0.1", | ||
"standard-version": "^4.4.0", | ||
"webpack": "^4.12.2", | ||
@@ -52,6 +51,12 @@ "webpack-cli": "^3.0.8", | ||
"dependencies": { | ||
"d3-geo": "^1.11.3", | ||
"robust-point-in-polygon": "^1.0.3", | ||
"topojson-client": "^3.0.0" | ||
}, | ||
"version": "0.0.6" | ||
"version": "0.0.7", | ||
"browserslist": [ | ||
">0.2%", | ||
"not dead", | ||
"not ie <= 11", | ||
"not op_mini all" | ||
] | ||
} |
# Lightning Maps (*Alpha release*) | ||
A lightweight, minimal-dependecy slippy map renderer. | ||
A fast, lightweight slippy map renderer with very minimal dependencies. | ||
@@ -10,5 +10,6 @@ Heavily inspired by [Pigeon Maps](https://github.com/mariusandra/pigeon-maps) and [Leaflet](https://leafletjs.com), but with slightly different goals in mind: | ||
* Modern, built using ES6+ syntax | ||
* Lightweight, [minimal dependencies](https://github.com/Geocodio/lightning-maps/blob/master/package.json#L28) with a [minified bundle](https://raw.githubusercontent.com/Geocodio/lightning-maps/master/lib/LightningMaps.min.js) of less than 20kb | ||
* Lightweight, minimal dependencies | ||
* Ability to render thousands of markers, by using `<canvas>` rendering instead of depending on the DOM | ||
* Wrappers for React and VueJS (Coming soon) | ||
* Supports rendering of complex polygons | ||
* Wrapper for React (VueJS coming soon) | ||
@@ -20,3 +21,3 @@ ## Using | ||
``` | ||
npm install --save npm lightning-maps | ||
yarn add lightning-maps | ||
``` | ||
@@ -27,3 +28,3 @@ | ||
```html | ||
<script src="https://unpkg.com/lightning-maps@0.0.1/lib/LightningMaps.min.js"></script> | ||
<script src="https://unpkg.com/lightning-maps@0.0.7/lib/LightningMaps.min.js"></script> | ||
``` | ||
@@ -60,7 +61,11 @@ | ||
```bash | ||
npm run dev | ||
npm run test:watch | ||
yarn run dev | ||
yarn run test:watch | ||
``` | ||
> You can now head to [http://localhost:8080/simple.html](http://localhost:8080/simple.html) or [http://localhost:8080/markers.html](http://localhost:8080/markers.html) to test the app | ||
### Development urls: | ||
* [http://localhost:8080/simple.html](http://localhost:8080/simple.html) | ||
* [http://localhost:8080/markers.html](http://localhost:8080/markers.html) | ||
* [http://localhost:8080/polygons.html](http://localhost:8080/polygons.html) | ||
* [http://localhost:8080/events.html](http://localhost:8080/events.html) | ||
@@ -70,3 +75,3 @@ ### Build library for distribution | ||
```bash | ||
npm run build | ||
yarn run build | ||
``` |
@@ -46,4 +46,5 @@ export const defaultMapOptions = { | ||
* Used for debouncing events such as scrolling | ||
* Note: Needs to be greater than the animationDurationMs value | ||
*/ | ||
debounceIntervalMs: 200, | ||
debounceIntervalMs: 350, | ||
@@ -80,6 +81,40 @@ /** | ||
/** | ||
* What color should the polygon be? | ||
* Whether the polygon should have a stroked line | ||
*/ | ||
enableStroke: true, | ||
/** | ||
* What color should the polygon lines be? | ||
* Supports hex, rgb and rgba values | ||
*/ | ||
color: 'rgba(0, 0, 200, 0.7)' | ||
strokeStyle: 'rgba(50, 25, 50, 1.0)', | ||
/** | ||
* Specify distances to alternately draw a line and a gap to form | ||
* a dashed or dotted line. Line will be solid if array is empty | ||
*/ | ||
lineDash: [], | ||
/** | ||
* Specify the thickness of polygon lines. The width scales with the zoom level, so the actual | ||
* width in pixels is: lineWidth * zoom | ||
*/ | ||
lineWidth: 0.25, | ||
/** | ||
* Whether the polygon should be filled with a color | ||
*/ | ||
enableFill: true, | ||
/** | ||
* What color should the polygon be filled with? | ||
* Supports hex, rgb and rgba values | ||
*/ | ||
fillStyle: 'rgba(0, 0, 0, 0.2)' | ||
}; | ||
export const defaultPolygonHoverOptions = { | ||
strokeStyle: 'red', | ||
lineWidth: 0.5 | ||
}; | ||
@@ -5,3 +5,3 @@ import Map from './Map'; | ||
export default { | ||
export { | ||
Map, | ||
@@ -8,0 +8,0 @@ Marker, |
323
src/Map.js
@@ -20,6 +20,12 @@ import TileConversion from './TileConversion'; | ||
this.attachEvents(); | ||
this.applyStyles(); | ||
this.lastDrawState = null; | ||
/** | ||
* Events | ||
*/ | ||
this.onMarkerClicked = null; | ||
this.onMarkerHover = null; | ||
this.onPolygonHover = null; | ||
this.draw = this.draw.bind(this); | ||
@@ -38,3 +44,3 @@ window.requestAnimationFrame(this.draw); | ||
dragStartPosition: null, | ||
lastEventActionTime: null, | ||
lastZoomEventActionTime: null, | ||
startZoom: this.options.zoom, | ||
@@ -50,3 +56,4 @@ targetZoom: this.options.zoom, | ||
new TileLayer(this) | ||
] | ||
], | ||
mousePosition: { x: 0, y: 0} | ||
}; | ||
@@ -60,7 +67,9 @@ } | ||
setZoom(zoom) { | ||
if (this.zoomValueIsValid(zoom)) { | ||
if (this.zoomValueIsValid(zoom) && this.isReadyForZoomEvent()) { | ||
zoom = Math.round(zoom); | ||
this.state.tileLayers.push(new TileLayer(this, zoom)); | ||
// this.state.tileLayers[0].tilesZoomLevel = this.options.zoom; | ||
this.state.lastEventActionTime = window.performance.now(); | ||
this.state.lastZoomEventActionTime = window.performance.now(); | ||
this.state.zoomAnimationStart = window.performance.now(); | ||
@@ -108,4 +117,4 @@ this.state.targetZoom = zoom; | ||
isReadyForEvent() { | ||
if (!this.state.lastEventActionTime) { | ||
isReadyForZoomEvent() { | ||
if (!this.state.lastZoomEventActionTime) { | ||
return true; | ||
@@ -115,3 +124,3 @@ } | ||
const now = window.performance.now(); | ||
const milliSecondsSinceLastEvent = now - this.state.lastEventActionTime; | ||
const milliSecondsSinceLastEvent = now - this.state.lastZoomEventActionTime; | ||
@@ -129,8 +138,6 @@ return milliSecondsSinceLastEvent > this.options.debounceIntervalMs; | ||
if (this.isReadyForEvent()) { | ||
if (event.deltaY > 5) { | ||
this.setZoom(this.options.zoom - 1); | ||
} else if (event.deltaY < -5) { | ||
this.setZoom(this.options.zoom + 1); | ||
} | ||
if (event.deltaY > 5) { | ||
this.setZoom(this.options.zoom - 1); | ||
} else if (event.deltaY < -5) { | ||
this.setZoom(this.options.zoom + 1); | ||
} | ||
@@ -142,8 +149,7 @@ }); | ||
const centerX = this.state.canvasDimensions[0] / 2; | ||
const centerY = this.state.canvasDimensions[1] / 2; | ||
const canvasCenter = this.getCanvasCenter(); | ||
this.setTargetMoveOffset( | ||
-(event.clientX - centerX), | ||
-(event.clientY - centerY) | ||
-(event.clientX - canvasCenter[0]), | ||
-(event.clientY - canvasCenter[1]) | ||
); | ||
@@ -157,8 +163,10 @@ | ||
this.state.mouseVelocities = []; | ||
if (!this.handleMouseEventInteraction(event, 'mousedown')) { | ||
this.state.mouseVelocities = []; | ||
this.state.dragStartPosition = [ | ||
event.clientX - this.state.moveOffset[0], | ||
event.clientY - this.state.moveOffset[1] | ||
]; | ||
this.state.dragStartPosition = [ | ||
event.clientX - this.state.moveOffset[0], | ||
event.clientY - this.state.moveOffset[1] | ||
]; | ||
} | ||
}); | ||
@@ -169,36 +177,40 @@ | ||
const x = -(this.state.dragStartPosition[0] - event.clientX); | ||
const y = -(this.state.dragStartPosition[1] - event.clientY); | ||
if (!this.state.dragStartPosition) { | ||
this.handleMouseEventInteraction(event, 'mouseup'); | ||
} else { | ||
const x = -(this.state.dragStartPosition[0] - event.clientX); | ||
const y = -(this.state.dragStartPosition[1] - event.clientY); | ||
if (this.state.moveOffset[0] !== 0 || this.state.moveOffset[1] !== 0) { | ||
const now = window.performance.now(); | ||
const timingThreshold = now - this.options.throwTimingThresholdMs; | ||
if (this.state.moveOffset[0] !== 0 || this.state.moveOffset[1] !== 0) { | ||
const now = window.performance.now(); | ||
const timingThreshold = now - this.options.throwTimingThresholdMs; | ||
const thresholdsToConsider = this.state.mouseVelocities | ||
.filter(threshold => threshold[0] > timingThreshold) | ||
.map(threshold => threshold[1]); | ||
const thresholdsToConsider = this.state.mouseVelocities | ||
.filter(threshold => threshold[0] > timingThreshold) | ||
.map(threshold => threshold[1]); | ||
const velocitySum = thresholdsToConsider.reduce( | ||
(accumulator, velocity) => accumulator + velocity, | ||
0 | ||
); | ||
const velocitySum = thresholdsToConsider.reduce( | ||
(accumulator, velocity) => accumulator + velocity, | ||
0 | ||
); | ||
const averageVelocity = velocitySum / thresholdsToConsider.length; | ||
const averageVelocity = velocitySum / thresholdsToConsider.length; | ||
if (averageVelocity >= this.options.throwVelocityThreshold) { | ||
let multiplier = averageVelocity / this.options.throwVelocityThreshold | ||
* this.options.panAccelerationMultiplier; | ||
if (averageVelocity >= this.options.throwVelocityThreshold) { | ||
let multiplier = averageVelocity / this.options.throwVelocityThreshold | ||
* this.options.panAccelerationMultiplier; | ||
multiplier = Math.min(multiplier, this.options.maxPanAcceleration); | ||
multiplier = Math.min(multiplier, this.options.maxPanAcceleration); | ||
this.setTargetMoveOffset( | ||
x * multiplier, | ||
y * multiplier | ||
); | ||
} else { | ||
this.updateCenter(); | ||
this.setTargetMoveOffset( | ||
x * multiplier, | ||
y * multiplier | ||
); | ||
} else { | ||
this.updateCenter(); | ||
} | ||
} | ||
this.state.dragStartPosition = null; | ||
} | ||
this.state.dragStartPosition = null; | ||
}); | ||
@@ -224,2 +236,4 @@ | ||
this.state.lastMouseMoveEvent = window.performance.now(); | ||
} else { | ||
this.handleMouseEventInteraction(event, 'mousemove'); | ||
} | ||
@@ -231,6 +245,2 @@ | ||
applyStyles() { | ||
this.canvas.style.cursor = 'grab'; | ||
} | ||
easeOutQuad(time) { | ||
@@ -256,4 +266,3 @@ return time * (2 - time); | ||
this.options.zoom, | ||
this.options.tileSize, | ||
this.state.canvasDimensions | ||
this.options.tileSize | ||
); | ||
@@ -383,5 +392,6 @@ } | ||
this.drawMarkers(); | ||
this.drawPolygons(); | ||
this.drawAttribution(); | ||
this.renderPolygons(); | ||
this.renderMarkers(); | ||
this.renderControls(); | ||
this.renderAttribution(); | ||
} | ||
@@ -393,4 +403,6 @@ | ||
getMapBounds() { | ||
const canvasCenter = this.getCanvasCenter(); | ||
const nw = TileConversion.pixelToLatLon( | ||
[this.state.canvasDimensions[0] / 2, (this.state.canvasDimensions[1] / 2)], | ||
[canvasCenter[0], canvasCenter[1]], | ||
this.options.center, | ||
@@ -402,3 +414,3 @@ this.options.zoom, | ||
const se = TileConversion.pixelToLatLon( | ||
[-this.state.canvasDimensions[0] / 2, -(this.state.canvasDimensions[1] / 2)], | ||
[-canvasCenter[0], -canvasCenter[1]], | ||
this.options.center, | ||
@@ -414,15 +426,22 @@ this.options.zoom, | ||
drawMarkers() { | ||
getVisibleMarkers() { | ||
const bounds = this.getMapBounds(); | ||
const visibleMarkers = this.state.markers.filter(marker => { | ||
return this.state.markers.filter(marker => { | ||
return marker.coords[0] <= bounds.nw[0] && marker.coords[0] >= bounds.se[0] | ||
&& marker.coords[1] >= bounds.nw[1] && marker.coords[1] <= bounds.se[1]; | ||
}); | ||
} | ||
const center = [ | ||
getCanvasCenter() { | ||
return [ | ||
this.state.canvasDimensions[0] / 2, | ||
this.state.canvasDimensions[1] / 2 | ||
]; | ||
} | ||
renderMarkers() { | ||
const visibleMarkers = this.getVisibleMarkers(); | ||
const canvasCenter = this.getCanvasCenter(); | ||
visibleMarkers.map(marker => { | ||
@@ -433,9 +452,8 @@ const position = TileConversion.latLonToPixel( | ||
this.options.zoom, | ||
this.options.tileSize, | ||
this.state.canvasDimensions | ||
this.options.tileSize | ||
); | ||
marker.render(this.context, [ | ||
center[0] - position[0] + this.state.moveOffset[0], | ||
center[1] - position[1] + this.state.moveOffset[1] | ||
canvasCenter[0] - position[0] + this.state.moveOffset[0], | ||
canvasCenter[1] - position[1] + this.state.moveOffset[1] | ||
]); | ||
@@ -445,6 +463,7 @@ }); | ||
drawPolygons() { | ||
renderPolygons() { | ||
const mapState = new MapState( | ||
this.options.center, | ||
this.options.zoom, | ||
this.state.targetZoom, | ||
this.options.tileSize, | ||
@@ -457,9 +476,164 @@ this.state.canvasDimensions, | ||
polygon.render(this.context, mapState); | ||
polygon.handleMouseOver(this.context, mapState, this.state.mousePosition); | ||
}); | ||
} | ||
drawAttribution() { | ||
handleMouseEventInteraction(event, name) { | ||
this.state.mousePosition = { | ||
x: event.clientX, | ||
y: event.clientY | ||
}; | ||
const controlObjects = this.getControlObjects().filter(item => this.isMouseOverObject(item.bounds)); | ||
const markers = controlObjects.length <= 0 && (this.onMarkerClicked || this.onMarkerHover) | ||
? this.getMarkersBounds().filter(item => this.isMouseOverObject(item.bounds)) | ||
: []; | ||
if (name === 'mouseup') { | ||
if (controlObjects.length > 0) { | ||
const controlObject = controlObjects[0]; | ||
this.setZoom(controlObject.label === '+' | ||
? this.options.zoom + 1 | ||
: this.options.zoom - 1); | ||
} | ||
if (this.onMarkerClicked) { | ||
markers.map(item => this.onMarkerClicked(item.marker)); | ||
} | ||
} else { | ||
if (this.onMarkerHover) { | ||
markers.map(item => this.onMarkerHover(item.marker)); | ||
} | ||
} | ||
let anyItemIsHover = controlObjects.length > 0 || markers.length > 0; | ||
let polygons = []; | ||
if (!anyItemIsHover) { | ||
const mapState = new MapState( | ||
this.options.center, | ||
this.options.zoom, | ||
this.state.targetZoom, | ||
this.options.tileSize, | ||
this.state.canvasDimensions, | ||
this.state.moveOffset | ||
); | ||
polygons = this.state.polygons.map(polygon => | ||
polygon.handleMouseOver(this.context, mapState, this.state.mousePosition) | ||
).filter(polygon => polygon.length > 0); | ||
} | ||
if (polygons.length > 0) { | ||
anyItemIsHover = true; | ||
if (this.onPolygonHover) { | ||
polygons.map(polygon => polygon.map(item => this.onPolygonHover(item))); | ||
} | ||
} | ||
this.canvas.style.cursor = anyItemIsHover | ||
? 'pointer' | ||
: 'grab'; | ||
return anyItemIsHover; | ||
} | ||
getControlObjects() { | ||
const margin = 4; | ||
const size = 30; | ||
return [ | ||
{ | ||
bounds: { | ||
x: margin, | ||
y: margin, | ||
width: size, | ||
height: size | ||
}, | ||
label: '+' | ||
}, | ||
{ | ||
bounds: { | ||
x: margin, | ||
y: margin + size + margin, | ||
width: size, | ||
height: size | ||
}, | ||
label: '-' | ||
} | ||
]; | ||
} | ||
getMarkersBounds() { | ||
const visibleMarkers = this.getVisibleMarkers(); | ||
const canvasCenter = this.getCanvasCenter(); | ||
return visibleMarkers.map(marker => { | ||
const position = TileConversion.latLonToPixel( | ||
marker.coords, | ||
this.options.center, | ||
this.options.zoom, | ||
this.options.tileSize | ||
); | ||
const markerSize = marker.size; | ||
const markerOffset = marker.offset; | ||
return { | ||
bounds: { | ||
x: canvasCenter[0] - position[0] + this.state.moveOffset[0] - (markerSize[0] / 2) + markerOffset[0], | ||
y: canvasCenter[1] - position[1] + this.state.moveOffset[1] - (markerSize[1] / 2) + markerOffset[1], | ||
width: markerSize[0], | ||
height: markerSize[1] | ||
}, | ||
marker | ||
}; | ||
}); | ||
} | ||
renderControls() { | ||
this.getControlObjects().map(item => this.renderControl(item.bounds, item.label)); | ||
} | ||
renderControl(bounds, label) { | ||
const radius = 10; | ||
// Background | ||
this.context.fillStyle = this.isMouseOverObject(bounds) | ||
? 'rgba(100, 100, 100, 0.7)' | ||
: 'rgba(0, 0, 0, 0.7)'; | ||
this.roundedRectangle(bounds.x, bounds.y, bounds.width, bounds.height, radius); | ||
// Text | ||
this.context.font = 'bold 25px courier'; | ||
this.context.textAlign = 'center'; | ||
this.context.textBaseline = 'middle'; | ||
this.context.fillStyle = 'rgba(255, 255, 255)'; | ||
this.context.fillText( | ||
label, | ||
bounds.x + (bounds.width / 2), | ||
bounds.y + (bounds.height / 2) | ||
); | ||
} | ||
isMouseOverObject(bounds) { | ||
return this.state.mousePosition.x >= bounds.x | ||
&& this.state.mousePosition.x <= bounds.x + bounds.width | ||
&& this.state.mousePosition.y >= bounds.y | ||
&& this.state.mousePosition.y <= bounds.y + bounds.height; | ||
} | ||
renderAttribution() { | ||
const margin = 4; | ||
this.context.font = 'bold 12px sans-serif'; | ||
this.context.textAlign = 'left'; | ||
this.context.textBaseline = 'alphabetic'; | ||
const textBounds = this.context.measureText(this.options.attribution); | ||
@@ -475,8 +649,5 @@ | ||
this.context.fillText(this.options.attribution, x, y); | ||
} | ||
roundedRectangle(x, y, width, height) { | ||
const radius = 5; | ||
roundedRectangle(x, y, width, height, radius = 5) { | ||
this.context.beginPath(); | ||
@@ -505,2 +676,6 @@ this.context.moveTo(x + radius, y); | ||
setMarkers(markers) { | ||
this.state.markers = markers; | ||
} | ||
addPolygon(polygon) { | ||
@@ -510,2 +685,6 @@ this.state.polygons.push(polygon); | ||
setPolygons(polygons) { | ||
this.state.polygons = polygons; | ||
} | ||
} |
export default class MapState { | ||
constructor(center, zoom, tileSize, canvasDimensions, moveOffset) { | ||
constructor(center, zoom, targetZoom, tileSize, canvasDimensions, moveOffset) { | ||
this._center = center; | ||
this._zoom = zoom; | ||
this._targetZoom = targetZoom; | ||
this._tileSize = tileSize; | ||
@@ -18,2 +19,6 @@ this._canvasDimensions = canvasDimensions; | ||
get targetZoom() { | ||
return this._targetZoom; | ||
} | ||
get tileSize() { | ||
@@ -20,0 +25,0 @@ return this._tileSize; |
@@ -17,2 +17,37 @@ import { defaultMarkerOptions } from './defaultOptions'; | ||
get size() { | ||
switch (this.options.type) { | ||
case 'marker': | ||
return [ | ||
17.698069, | ||
24.786272 | ||
]; | ||
case 'circle': | ||
return [10, 10]; | ||
case 'donut': | ||
return [14, 14]; | ||
case 'image': | ||
return this.options.image | ||
? [this.options.image.width, this.options.image.height] | ||
: null; | ||
default: | ||
return null; | ||
} | ||
} | ||
get offset() { | ||
if (this.options.type === 'marker') { | ||
return [ | ||
0, | ||
-(this.size[1] / 2) | ||
]; | ||
} | ||
return [0, 0]; | ||
} | ||
render(context, position) { | ||
@@ -33,2 +68,6 @@ let renderFunction = null; | ||
break; | ||
case 'image': | ||
renderFunction = this.renderImage; | ||
break; | ||
} | ||
@@ -50,3 +89,3 @@ | ||
context.beginPath(); | ||
context.arc(position[0], position[1], 5, 0, 2 * Math.PI); | ||
context.arc(position[0], position[1], this.size[0] / 2, 0, 2 * Math.PI); | ||
context.fill(); | ||
@@ -60,3 +99,3 @@ context.restore(); | ||
context.lineWidth = 5; | ||
context.arc(position[0], position[1], 7, 0, 2 * Math.PI); | ||
context.arc(position[0], position[1], this.size[0] / 2, 0, 2 * Math.PI); | ||
context.stroke(); | ||
@@ -67,7 +106,6 @@ context.restore(); | ||
renderMarker(context, position) { | ||
const markerWidth = 17.698069; | ||
const markerHeight = 24.786272; | ||
const size = this.size; | ||
const x = position[0] - markerWidth / 2; | ||
const y = position[1] - markerHeight; | ||
const x = position[0] - size[0] / 2; | ||
const y = position[1] - size[1]; | ||
@@ -91,2 +129,12 @@ context.save(); | ||
} | ||
renderImage(context, position) { | ||
if (this.options.image) { | ||
const size = this.size; | ||
const x = position[0] - size[0] / 2; | ||
const y = position[1] - size[1] / 2; | ||
context.drawImage(this.options.image, x, y, size[0], size[1]); | ||
} | ||
} | ||
} |
@@ -1,26 +0,14 @@ | ||
import { defaultPolygonOptions } from './defaultOptions'; | ||
import { defaultPolygonOptions, defaultPolygonHoverOptions } from './defaultOptions'; | ||
import TileConversion from './TileConversion'; | ||
import { geoPath, geoTransform } from 'd3-geo'; | ||
import { mesh } from 'topojson-client'; | ||
import { feature } from 'topojson-client'; | ||
import classifyPoint from 'robust-point-in-polygon'; | ||
const POLYGON_CACHE = {}; | ||
export default class Polygon { | ||
constructor(sourceUrl, options = {}) { | ||
this._sourceUrl = sourceUrl; | ||
constructor(json, objectName, options = {}, hoverOptions = null) { | ||
this._options = Object.assign({}, defaultPolygonOptions, options); | ||
this._geometry = null; | ||
this._hoverOptions = Object.assign({}, defaultPolygonOptions, defaultPolygonHoverOptions, hoverOptions); | ||
fetch(this._sourceUrl) | ||
.then(response => response.json()) | ||
.then(json => { | ||
this._geometry = json; | ||
}) | ||
.catch(err => console.log(`Could not load ${this._sourceUrl}: ${err.message || err}`)); | ||
this.geometry = feature(json, json.objects[objectName]); | ||
} | ||
get sourceUrl() { | ||
return this._sourceUrl; | ||
} | ||
get options() { | ||
@@ -30,57 +18,306 @@ return this._options; | ||
get hoverOptions() { | ||
return this._hoverOptions; | ||
} | ||
get geometry() { | ||
return window._geometry; | ||
} | ||
set geometry(geometry) { | ||
window._geometry = geometry; | ||
} | ||
get projectedGeometry() { | ||
return window._projectedGeometry; | ||
} | ||
set projectedGeometry(projectedGeometry) { | ||
window._projectedGeometry = projectedGeometry; | ||
} | ||
get projectedGeometryState() { | ||
return this._projectedGeometryState; | ||
} | ||
set projectedGeometryState(projectedGeometryState) { | ||
this._projectedGeometryState = projectedGeometryState; | ||
} | ||
handleMouseOver(context, mapState, mousePosition) { | ||
const currentlyZooming = Math.round(mapState.zoom) !== mapState.zoom; | ||
if (!this.geometry || !this.projectedGeometry || currentlyZooming) { | ||
return []; | ||
} | ||
const canvasCenter = [ | ||
mapState.canvasDimensions[0] / 2, | ||
mapState.canvasDimensions[1] / 2 | ||
]; | ||
const originZoom = this.determineOriginZoom(mapState); | ||
const centerOffset = this.calculateCenterOffset(mapState, originZoom); | ||
const point = [ | ||
mousePosition.x - centerOffset[0] - canvasCenter[0], | ||
mousePosition.y - centerOffset[1] - canvasCenter[1] | ||
]; | ||
return this.projectedGeometry.filter(item => { | ||
const isHover = item.geometry.filter(list => classifyPoint(list, point) === -1).length > 0; | ||
if (isHover) { | ||
context.beginPath(); | ||
item.geometry.map((list) => { | ||
list.map((position, index) => { | ||
position = [ | ||
position[0] + centerOffset[0] + canvasCenter[0], | ||
position[1] + centerOffset[1] + canvasCenter[1] | ||
]; | ||
if (index === 0) { | ||
context.moveTo(position[0], position[1]); | ||
} else { | ||
context.lineTo(position[0], position[1]); | ||
} | ||
}); | ||
}); | ||
this.applyContextStyles(context, this.hoverOptions, mapState.zoom); | ||
if (this.options.enableStroke) context.fill(); | ||
if (this.options.enableFill) context.stroke(); | ||
} | ||
return isHover; | ||
}); | ||
} | ||
calculateCenterOffset(mapState, originZoom) { | ||
return [ | ||
-(TileConversion.lon2tile(mapState.center[1], originZoom, false) * mapState.tileSize), | ||
-(TileConversion.lat2tile(mapState.center[0], originZoom, false) * mapState.tileSize) | ||
]; | ||
} | ||
render(context, mapState) { | ||
if (!this._geometry) { | ||
if (!this.geometry) { | ||
return; | ||
} | ||
context.fillStyle = this.options.color; | ||
context.strokeStyle = this.options.color; | ||
const originZoom = this.determineOriginZoom(mapState); | ||
this.mapState = mapState; | ||
const zoomDiff = originZoom | ||
? mapState.zoom - originZoom | ||
: 0; | ||
const center = [ | ||
this.mapState.canvasDimensions[0] / 2, | ||
this.mapState.canvasDimensions[1] / 2 | ||
const scale = zoomDiff !== 0 | ||
? Math.pow(2, zoomDiff) | ||
: 1; | ||
const mapCenterChanged = this.renderedMapCenter !== mapState.center, | ||
mapZoomChanged = (zoomDiff === 0 && this.renderedZoomLevel !== mapState.zoom); | ||
const shouldReRender = mapCenterChanged || mapZoomChanged; | ||
let centerOffset = null; | ||
if (shouldReRender) { | ||
this.renderedZoomLevel = mapState.zoom; | ||
this.renderedMapCenter = mapState.center; | ||
this.projectedGeometry = this.geometry.features.map(feature => { | ||
return { | ||
...feature, | ||
geometry: this.projectGeometry(feature.geometry, mapState) | ||
}; | ||
}); | ||
centerOffset = this.calculateCenterOffset(mapState, originZoom); | ||
this.calculatePolygonExtends(centerOffset); | ||
} | ||
const canvasCenter = [ | ||
mapState.canvasDimensions[0] / 2, | ||
mapState.canvasDimensions[1] / 2 | ||
]; | ||
const transform = geoTransform({point: this.projectPoint, mapState, center }); | ||
const imagePosition = [ | ||
canvasCenter[0] + mapState.moveOffset[0] + (this.polygonExtends.minX * scale), | ||
canvasCenter[1] + mapState.moveOffset[1] + (this.polygonExtends.minY * scale) | ||
]; | ||
const path = geoPath(transform).context(context); | ||
const imageRect = { | ||
left: Math.floor(imagePosition[0] * -1), | ||
right: Math.ceil(Math.abs(imagePosition[0]) + mapState.canvasDimensions[0]), | ||
top: Math.floor(imagePosition[1] * -1), | ||
bottom: Math.ceil(Math.abs(imagePosition[1]) + mapState.canvasDimensions[1]) | ||
}; | ||
context.beginPath(); | ||
path(mesh(this._geometry)); | ||
context.stroke(); | ||
if (shouldReRender) { | ||
this.renderOffscreenCanvas(mapState, centerOffset, imageRect); | ||
} | ||
const imageDrawPosition = [ | ||
mapState.moveOffset[0] - (canvasCenter[0] * (scale - 1)), | ||
mapState.moveOffset[1] - (canvasCenter[1] * (scale - 1)) | ||
]; | ||
const scaledWidth = this.polygonDimensions[0] * scale, | ||
scaledHeight = this.polygonDimensions[1] * scale; | ||
context.drawImage( | ||
this.offscreenCanvas, | ||
imageDrawPosition[0], imageDrawPosition[1], | ||
scaledWidth, scaledHeight | ||
); | ||
} | ||
projectPoint(x, y) { | ||
const cachedPosition = (x, y, mapState) => { | ||
const cacheKey = JSON.stringify([ | ||
[y, x], this.mapState.center, this.mapState.zoom, | ||
this.mapState.tileSize, this.mapState.canvasDimensions | ||
]); | ||
determineOriginZoom(mapState) { | ||
let originZoom = mapState.zoom; | ||
if (cacheKey in POLYGON_CACHE) { | ||
return POLYGON_CACHE[cacheKey]; | ||
if (mapState.targetZoom > mapState.zoom) { // Zoming in | ||
originZoom = Math.floor(mapState.zoom); | ||
} else if (mapState.targetZoom < mapState.zoom) { // Zooming out | ||
originZoom = Math.ceil(mapState.zoom); | ||
} | ||
return originZoom; | ||
} | ||
createOffscreenCanvas(clipRect) { | ||
this.polygonDimensions = [ | ||
clipRect.right - clipRect.left, | ||
clipRect.bottom - clipRect.top | ||
]; | ||
this.offscreenCanvas = document.createElement('canvas'); | ||
this.offscreenCanvas.width = this.polygonDimensions[0]; | ||
this.offscreenCanvas.height = this.polygonDimensions[1]; | ||
return this.offscreenCanvas.getContext('2d'); | ||
} | ||
calculatePolygonExtends(centerOffset) { | ||
let minX, maxX, minY, maxY = null; | ||
this.mapGeometry(position => { | ||
position = [ | ||
position[0] + centerOffset[0], | ||
position[1] + centerOffset[1] | ||
]; | ||
if (!maxX || position[0] > maxX) { | ||
maxX = position[0]; | ||
} | ||
const position = TileConversion.latLonToPixel( | ||
[y, x], | ||
this.mapState.center, | ||
this.mapState.zoom, | ||
this.mapState.tileSize, | ||
this.mapState.canvasDimensions | ||
); | ||
if (!minX || position[0] < minX) { | ||
minX = position[0]; | ||
} | ||
POLYGON_CACHE[cacheKey] = position; | ||
if (!maxY || position[1] > maxY) { | ||
maxY = position[1]; | ||
} | ||
return position; | ||
if (!minY || position[1] < minY) { | ||
minY = position[1]; | ||
} | ||
}); | ||
this.polygonDimensions = [ | ||
Math.ceil(maxX - minX), | ||
Math.ceil(maxY - minY) | ||
]; | ||
this.polygonExtends = { | ||
minX, maxX, | ||
minY, maxY | ||
}; | ||
} | ||
const position = cachedPosition(x, y, this.mapState); | ||
renderOffscreenCanvas(mapState, centerOffset, clipRect) { | ||
const offscreenContext = this.createOffscreenCanvas(clipRect); | ||
const projectedX = this.center[0] - position[0] + this.mapState.moveOffset[0]; | ||
const projectedY = this.center[1] - position[1] + this.mapState.moveOffset[1]; | ||
offscreenContext.beginPath(); | ||
offscreenContext.font = 'bold 8px helvetica'; | ||
this.stream.point(projectedX, projectedY); | ||
this.projectedGeometry.map((item) => | ||
item.geometry.map((list) => { | ||
const pointsInClipRect = list.filter(position => { | ||
position = [ | ||
position[0] - this.polygonExtends.minX + centerOffset[0], | ||
position[1] - this.polygonExtends.minY + centerOffset[1] | ||
]; | ||
return position[0] >= clipRect.left && position[0] <= clipRect.right | ||
&& position[1] >= clipRect.top && position[1] <= clipRect.bottom; | ||
}); | ||
if (pointsInClipRect.length > 0) { | ||
list.map((position, index) => { | ||
position = [ | ||
position[0] - this.polygonExtends.minX + centerOffset[0] - clipRect.left, | ||
position[1] - this.polygonExtends.minY + centerOffset[1] - clipRect.top | ||
]; | ||
if (index === 0) { | ||
// offscreenContext.fillText(item.properties.NAME, position[0], position[1]); | ||
offscreenContext.moveTo(position[0], position[1]); | ||
} else { | ||
offscreenContext.lineTo(position[0], position[1]); | ||
} | ||
}); | ||
} | ||
}) | ||
); | ||
this.applyContextStyles(offscreenContext, this.options, mapState.zoom); | ||
if (this.options.enableStroke) offscreenContext.fill(); | ||
if (this.options.enableFill) offscreenContext.stroke(); | ||
} | ||
applyContextStyles(context, options, zoom) { | ||
context.fillStyle = options.fillStyle; | ||
context.strokeStyle = options.strokeStyle; | ||
context.lineWidth = options.lineWidth * zoom; | ||
context.setLineDash(options.lineDash); | ||
context.lineJoin = 'round'; | ||
} | ||
mapGeometry(pointCallback) { | ||
return this.projectedGeometry.map((item) => | ||
item.geometry.map((list) => | ||
list.map(pointCallback) | ||
) | ||
); | ||
} | ||
projectGeometry(geometry, mapState) { | ||
switch (geometry.type) { | ||
case 'Polygon': | ||
return [geometry.coordinates[0].map(item => this.projectPoint(mapState, item[0], item[1]))]; | ||
case 'MultiPolygon': | ||
return geometry.coordinates.map(coordinates => | ||
coordinates[0].map(item => this.projectPoint(mapState, item[0], item[1])) | ||
); | ||
} | ||
return []; | ||
} | ||
projectPoint(mapState, x, y) { | ||
const position = TileConversion.latLonToPixel( | ||
[y, x], | ||
null, | ||
mapState.zoom, | ||
mapState.tileSize | ||
); | ||
return [-position[0], -position[1]]; | ||
} | ||
} |
@@ -23,2 +23,12 @@ export default class Tile { | ||
} | ||
isValid() { | ||
const max = (1 << this.zoom); | ||
if (this.x >= max || this.x < 0 || this.y >= max || this.y < 0) { | ||
return false; | ||
} | ||
return true; | ||
} | ||
} |
@@ -59,9 +59,13 @@ // Based on https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#ECMAScript_.28JavaScript.2FActionScript.2C_etc..29 | ||
static latLonToPixel(coord, center, zoom, tileSize, mapDimensions) { | ||
static latLonToPixel(coord, center, zoom, tileSize) { | ||
const tileX = TileConversion.lon2tile(coord[1], zoom, false); | ||
const tileY = TileConversion.lat2tile(coord[0], zoom, false); | ||
const tileCenterX = TileConversion.lon2tile(center[1], zoom, false); | ||
const tileCenterY = TileConversion.lat2tile(center[0], zoom, false); | ||
let tileCenterX = 0, tileCenterY = 0; | ||
if (center) { | ||
tileCenterX = TileConversion.lon2tile(center[1], zoom, false); | ||
tileCenterY = TileConversion.lat2tile(center[0], zoom, false); | ||
} | ||
return [ | ||
@@ -68,0 +72,0 @@ -(tileX - tileCenterX) * tileSize, |
@@ -76,5 +76,7 @@ import TileConversion from './TileConversion'; | ||
if (tileX >= 0 && tileY >= 0) { | ||
grid[x][y] = new Tile(tileX, tileY, Math.round(this.tilesZoomLevel || options.zoom)); | ||
this.ensureTileAsset(grid[x][y]); | ||
const tile = new Tile(tileX, tileY, Math.round(this.tilesZoomLevel || options.zoom)); | ||
if (tile.isValid()) { | ||
this.ensureTileAsset(tile); | ||
grid[x][y] = tile; | ||
} | ||
@@ -185,4 +187,3 @@ } | ||
const totalTiles = horizontalTiles * verticalTiles; | ||
let loadedTiles = 0; | ||
let totalTiles = 0, loadedTiles = 0; | ||
@@ -193,3 +194,9 @@ for (let y = 0; y < verticalTiles; y++) { | ||
if (this.map.state.tiles[tile.id].loaded) { | ||
if (tile) { | ||
totalTiles++; | ||
} | ||
const cachedTile = tile && this.map.state.tiles[tile.id]; | ||
if (cachedTile && cachedTile.loaded) { | ||
loadedTiles++; | ||
@@ -196,0 +203,0 @@ } |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
1765665
16
36
7666
73
2
+ Addedrobust-orientation@1.2.1(transitive)
+ Addedrobust-point-in-polygon@1.0.3(transitive)
+ Addedrobust-scale@1.0.2(transitive)
+ Addedrobust-subtract@1.0.0(transitive)
+ Addedrobust-sum@1.0.0(transitive)
+ Addedtwo-product@1.0.2(transitive)
+ Addedtwo-sum@1.0.0(transitive)
- Removedd3-geo@^1.11.3
- Removedd3-array@1.2.4(transitive)
- Removedd3-geo@1.12.1(transitive)