We have a specific design challenge for polygon display within leaflet (latest version).
We have polygons which are rendered with a solid border as well as a semi-transparent background.
We are looking for a way to draw a solid borderline as well as a wider "inline" border and no background.
Note: the question is for polygons not rectangular. The below image
and code is just for example.
Is there any way to achieve this?
var polygon = L.polygon([
[ 51.72872938200587, -2.415618896484375 ],
[ 51.72872938200587, -2.080535888671875 ],
[ 51.901918172561714, -2.080535888671875 ],
[ 51.901918172561714, -2.415618896484375 ],
[ 51.72872938200587, -2.415618896484375 ]
],{
color:'#2F538F',
fillOpacity: 0.9,
fillColor: '#BFBFBF',
}).addTo(map);
This is achievable by utilizing leaftlet's class extension system.
To start with, leaflet's class diagram could be consulted to determine where the extension is needed. As a general rule, first try to extend classes towards the root, and prefer L.Class.extend over L.Class.include.
Working Solution:
Codesandbox
One approach is hooking into the rendering process. In the following example, L.Canvas is extended to a custom L.Canvas.WithExtraStyles class (leaflet's plugin building guidelines). The custom Renderer is then provided to map.
In this approach, note that multiple borders and fills (both inset and outset) could be provided using the extraStyles config.
extraStyle custom property accepts Array of PathOptions. With an additional inset, whose value could be positive or a negative number of pixels representing the offset form the border of the main geometry. A negative value of inset will put the border outside of the original polygon.
While implementing such customizations, special care must be taken to make sure leaflet is not considering the added customizations as separate geometric shapes. Otherwise interactive functionalities e.g. Polygon Edit or Leaflet Draw will have unexpected behaviour.
// CanvasWithExtraStyles.js
// First step is to provide a special renderer which accept configuration for extra borders.
// Here L.Canvas is extended using Leaflet's class system
const styleProperties = ['stroke', 'color', 'weight', 'opacity', 'fill', 'fillColor', 'fillOpacity'];
/*
* #class Polygon.MultiStyle
* #aka L.Polygon.MultiStyle
* #inherits L.Polygon
*/
L.Canvas.WithExtraStyles = L.Canvas.extend({
_updatePoly: function(layer, closed) {
const centerCoord = layer.getCenter();
const center = this._map.latLngToLayerPoint(centerCoord);
const originalParts = layer._parts.slice();
// Draw extra styles
if (Array.isArray(layer.options.extraStyles)) {
const originalStyleProperties = styleProperties.reduce(
(acc, cur) => ({ ...acc, [cur]: layer.options[cur] }),
{}
);
const cx = center.x;
const cy = center.y;
for (let eS of layer.options.extraStyles) {
const i = eS.inset || 0;
// For now, the algo doesn't support MultiPolygon
// To have it support MultiPolygon, find centroid
// of each MultiPolygon and perform the following
layer._parts[0] = layer._parts[0].map(p => {
return {
x: p.x < cx ? p.x + i : p.x - i,
y: p.y < cy ? p.y + i : p.y - i
};
});
//Object.keys(eS).map(k => layer.options[k] = eS[k]);
Object.keys(eS).map(k => (layer.options[k] = eS[k]));
L.Canvas.prototype._updatePoly.call(this, layer, closed);
}
// Resetting original conf
layer._parts = originalParts;
Object.assign(layer.options, originalStyleProperties);
}
L.Canvas.prototype._updatePoly.call(this, layer, closed);
}
});
// Leaflet's conventions to also provide factory methods for classes
L.Canvas.withExtraStyles = function(options) {
return new L.Canvas.WithExtraStyles(options);
};
// --------------------------------------------------------------
// map.js
const map = L.map("map", {
center: [52.5145206, 13.3499977],
zoom: 18,
renderer: new L.Canvas.WithExtraStyles()
});
new L.tileLayer(
"https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_nolabels/{z}/{x}/{y}.png",
{
attribution: `attribution: '© OpenStreetMap, © CARTO`,
detectRetina: true
}
).addTo(map);
// Map center
const { x, y } = map.getSize();
// Left Polygon
const polyStyle1 = {
color: '#2f528f',
extraStyles: [
{
color: 'transparent',
weight: 10,
fillColor: '#d9d9d9'
}
]
};
// Sudo coordinates are generated form map container pixels
const polygonCoords1 = [
[0, 10],
[300, 10],
[300, 310],
[0, 310]
].map(point => map.containerPointToLatLng(point));
const polygon1 = new L.Polygon(polygonCoords1, polyStyle1);
polygon1.addTo(map);
// Right Polygon
const polyStyle2 = {
fillColor: "transparent",
color: '#2f528f',
extraStyles: [
{
inset: 6,
color: '#d9d9d9',
weight: 10
}
]
};
const polygonCoords2 = [
[340, 10],
[640, 10],
[640, 310],
[340, 310]
].map(point => map.containerPointToLatLng(point));
const polygon2 = new L.Polygon(polygonCoords2, polyStyle2);
polygon2.addTo(map);
<script src="https://unpkg.com/leaflet#1.6.0/dist/leaflet.js"></script>
<link href="https://unpkg.com/leaflet#1.6.0/dist/leaflet.css" rel="stylesheet"/>
<div id="map" style="width: 100vw; height: 100vw">0012</div>
Ideal Solution:
Implement a plugin as a separate npm module.
Try to extend or hook into Renderer itself instead of separately extending L.Canvas and L.SVG.
Hook the cusomization into base class Path instead of individual shapes: Polygon, Polyline or Circle.
Use the Recatngle/Polygon method.
// define rectangle geographical bounds
var bounds = [[54.559322, -5.767822], [56.1210604, -3.021240]];
// create an orange rectangle
L.rectangle(bounds, {}).addTo(map);
The Use options to get the desired effect on the lines. Options are inherited from polyline options
There you can tweak color, opacity, fill, fillColor, fillOpacity and fillRule to get the desired effect on the lines
Related
I use matter.js physic engine for my game. I updated body vertices with call setVertices but after call his collisions are not updated, so other object does not fall.
Maybe I skipped some basic principles of using matter.js so I present short version of using them, if this is not enough I will create a separate working example.
// Init engine
const engine = Engine.create();
const runner = Runner.create();
Runner.run(runner, engine);
// Add circle for check collision
const circle = Bodies.polygon(300, 100, 5, 12, {
frictionAir: 0, friction: 0.0001, restitution: 0.6
});
World.add(engine.world, circle);
// Create a body from vertex (parsed from svg path)
// Original vertexes
const mapVertexes = [[..]];
const ground = Bodies.fromVertices(400, 400, mapVertexes, { isStatic: true }, true);
World.add(engine.world, ground);
// On click to update vertices for polygon
const newVertices = [...]
Body.setVertices(ground, newVertices);
Before update vertices:
After update vertices:
All:
Perhaps I am not doing this the correct way. The way Leaflet draw works is that as soon as you draw something on a map using the draw control (circle, square, polygon, whatever) the delete/edit button becomes enabled. In my map, with clicks of links on my web page that contains the Leaflet map, a user can add many imageOverlays, but lets just say for this scenario the user clicked and added one imageOverlay. The imageOverlay appears fine on the map. Now the user wants to draw a shape - say a circle - on the map. When the user clicks on the circle icon in the draw control and paints it on the map the delete/edit button does NOT become enabled. I am stumped. I suspect that this may have something to do with layers /layering. I need to have that delete/edit button behave as it would if there were no imageLayers on the map. Can somebody please enlighten me? Thank you.
My React.js code adds the imageLayers to array and the array of images is then added to a L.featureGroup.
When drawing a shape, the layertype is added to the same L.featureGroup.
My code:
In React.js:
let myFeatureGroup = new L.FeatureGroup();
let imageLayersArray = [];
// My function to load imageLayers:
const applyLayer = (mapSubType) => {
const mapSubTypeCaps = mapSubType.toUpperCase();
let dlaBounds = null;
const someOptions = {
opacity: 0.3,
interactive: true,
bubblingMouseEvents: true,
className: 'imageborder',
zIndex: 5,
mapsubtypelayer: mapSubType, // required in order to identify a layer upon removal
};
// The 8 PNG image coordinates:
const ImageCoords = [
[[-90, 0], [0, 90]],
[[0, 0], [90, 90]],
[[-90, -90], [0, 0]],
[[0, -90], [90, 0]],
[[-180, 0], [-90, 90]],
[[90, 0], [180, 90]],
[[-180, -90], [-90, 0]],
[[90, -90], [180, 0]],
];
let counter = 0;
ImageCoords.map((coordEntry) => {
counter++;
const useThisURL = `https://dla-maps-storage.s3.amazonaws.com/DLAMAP${mapSubTypeCaps}${counter}.png`;
// Need to flip the coordinates from lng/lat to lat/lng for leaflet:
let arrayofcoordinates = [];
const flippedarrayofcoordinates = [];
arrayofcoordinates = coordEntry;
arrayofcoordinates.map((pairofcoordinates) => {
const workAry = [];
// flip the coordinates from lng/lat config to lat/lng:
workAry[0] = pairofcoordinates[1];
workAry[1] = pairofcoordinates[0];
flippedarrayofcoordinates.push(workAry);
});
dlaBounds = L.latLngBounds(flippedarrayofcoordinates);
const anImage = L.imageOverlay(
useThisURL,
dlaBounds,
someOptions,
)
// add the imagelayer to the imageLayersArray:
// Somebody along the way recommended to do this
imageLayersArray.push(anImage);
anImage.on('click', (leafletEvent) => {
handleImageClick(leafletEvent);
});
});
myFeatureGroup = L.featureGroup(imageLayersArray).addTo(map);
};
// Draw Event Created:
map.on(L.Draw.Event.CREATED, (e) => {
console.log('L.Draw.Event.CREATED start:: ');
const { layer } = e;
switch (e.layerType) { // types: polyline, polygon, rectangle
case 'circle':
// Call rest endpoint with lat/lng coordinates
break;
case 'marker':
// Call rest endpoint with lat/lng coordinates
break;
case 'polyline':
// Call rest endpoint with lat/lng coordinates
break;
default:
// POLYGON OR RECTANGLE DRAWN:
// Call rest endpoint with lat/lng coordinates
break;
}
myFeatureGroup.addLayer(layer);
console.log('L.Draw.Event.CREATED end:: ');
});
I have a simple fictional map that I want to control using Leaflet. It is a flat 2D plane and its "latitude/longitude"/coordinate system spans from [0,0] to [999,999].
I have customized the map as follows:
window.map = L.map('leaflet-map', {
crs: L.CRS.Simple,
center: [500, 500],
zoom: 13,
maxBounds: [
[0, 0],
[999, 999],
],
layers: [new MyLayer()],
});
To draw this map, I've created a new layer, MyLayer, which extends gridLayer:
export var MyLayer = GridLayer.extend({
createTile: function(coords, done) {
var error;
var xmlhttprequest = new XMLHttpRequest();
xmlhttprequest.addEventListener('readystatechange', function() {
done(error, dothething());
});
xmlhttprequest.open('GET', /* url */);
xmlhttprequest.send();
},
});
The problem I have is the URL accepts the [0,0] to [999,999] coordinate system as parameters but I can't find how to actually get those. I understand there may be some decimal element but I can floor that as appropriate.
When centered on [500, 500, 13] the coords object contains { x: 15516, y: -21558, z: 13 }. When passed to L.CRS.Simple.pointToLatLng(coords, coords.z) I get { lat: 2.631591796875, lng: 1.89404296875 }.
I've downloaded the source code in an attempt to understand how this transformation happens from Map._move(center, zoom, data) but all that appears to do is call this.options.crs.latLngToPoint(), which is exactly what I reverse in L.CRS.Simple.pointToLatLng. I'm frankly at a loss.
First of all, I encourage you to read the Leaflet tutorial on L.CRS.Simple once again. Let me quote a relevant bit from there :
In a CRS.Simple, one horizontal map unit is mapped to one horizontal pixel, and idem with vertical. [...] we can set minZoom to values lower than zero:
So you have no reason to go down to zoom level 13 on your L.CRS.Simple map by default, really. For a [0,0]-[999,999] map, use zoom level zero for an overview, or use map.fitBounds([[0,0],[999,999]]).
The values that the createTile() method receives are tile coordinates, not CRS coordinates. A level-0 tile is split into four level-1 tiles, sixteen level-2 tiles, 64 level-3 tiles, and so on, up to 2^13 tiles at level 13. This is easier to visualize by playing with a L.GridLayer that displays the tile coordinates, like:
var grid = L.gridLayer({
attribution: 'Grid Layer',
// tileSize: L.point(100, 100),
});
grid.createTile = function (coords) {
var tile = L.DomUtil.create('div', 'tile-coords');
tile.innerHTML = [coords.x, coords.y, coords.z].join(', ');
return tile;
};
map.addLayer(grid);
Second: you want to use the internal _tileCoordsToBounds method, defined at L.GridLayer. Give it a set of tile coordinates, and you'll get back a L.LatLngBounds with the area covered by such a tile.
The following example (try it live here) should put you on track. Remember to read the documentation for L.LatLngBounds as well.
var grid = L.gridLayer({
attribution: 'Grid Layer',
// tileSize: L.point(100, 100),
});
grid.createTile = function (coords) {
var tile = L.DomUtil.create('div', 'tile-coords');
var tileBounds = this._tileCoordsToBounds(coords);
tile.innerHTML = [coords.x, coords.y, coords.z].join(', ') +
"<br>" + tileBounds.toBBoxString();
return tile;
};
map.addLayer(grid);
I'm using LeafletJS to make a custom lab layout map and I plan to put in some rectangle layers to show whether or not areas are in use. Currently I have a working test case using coordinates to define each shape, but is there a way I can create a standard sized shape object which can be called and fed a single coordinate to center itself on?
Here is the current code from my Angular controller if it helps.
function showMap() {
var map = L.map('mapid', {
crs: L.CRS.Simple,
maxZoom: 4,
attributionControl: false
}).setView([0, 0], 1),
southWest = map.unproject([0, 4096], map.getMaxZoom()),
northEast = map.unproject([4096, 0], map.getMaxZoom()),
bounds = L.latLngBounds(southWest, northEast);
L.tileLayer('images/4231/{z}/{x}/{y}.png', {
minZoom: 1,
maxZoom: 4,
center: [0, 0],
noWrap: true,
bounds: bounds
}).addTo(map);
var testBench = [{
number: "1A1",
coord1: "-48.6",
coord2: "6",
coord3: "-81.4",
coord4: "71",
inUse: true
}, {
number: "1A2",
coord1: "-48.5",
coord2: "71",
coord3: "-81",
coord4: "137",
inUse: false
}, {
number: "1A3",
coord1: "-48.5",
coord2: "137",
coord3: "-81",
coord4: "202",
inUse: true
}];
angular.forEach(testBench, function(item, index) {
var location = [
[item.coord1, item.coord2],
[item.coord3, item.coord4]
],
color;
switch (item.inUse) {
case true:
color = "red"
break;
case false:
color = "green"
break;
}
L.rectangle(location, {
color: color,
weight: 1
}).bindPopup("Bench Number is: " + item.number).addTo(map);
})
map.setMaxBounds(bounds);
}
Eventually the bench info will be pulled from a DB rather than a variable and there will be hundreds of benches, so I'm looking to streamline the positioning layout as much as possible.
is there a way I can create a standard sized shape object which can be called and fed a single coordinate to center itself on?
In Leaflet, no.
Leaflet vector features, or L.Paths (L.Polylines and L.Polygons) are defined by their coordinates, not by their centroid and a series of offsets to that centroid.
You might want to implement a simple Factory design pattern to create regularly-shaped features giving only a center point, though.
I have a vector style, with a larger image: radius. I would like to have the select interaction's style match the vector style's image: radius.
How can I do so without manually redefining the whole editing style based on this page in the documentation?
Is it possible to take the default style and override only one part? Like the image's radius? Or at least redefine only the whole image?
Fiddle.
Share a style function between ol.layer.Vector and ol.interaction.Select and when selecting, change a variable that will be read within your function:
var radius = 10;
var styleFunction = function() {
return [
new ol.style.Style({
image: new ol.style.Circle({
radius: radius,
fill: new ol.style.Fill({
color: 'green'
})
})
})
];
};
var select_interaction = new ol.interaction.Select({
style: styleFunction
});
select_interaction.on('select', function(evt) {
radius = evt.selected.length > 0 ? 20 : 10;
});
I'm proposing a ol.style.Circle#setRadius that could be used in this case.