<title>Map with Base Map Switcher & Boolean Operations</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.3/dist/leaflet.css" crossorigin=""/>
<!-- Leaflet Draw CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css"/>
<!-- Leaflet Control Geocoder CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.css"/>
/* Styling for the custom boolean operations control */
<script src="https://unpkg.com/leaflet@1.9.3/dist/leaflet.js" crossorigin=""></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
<!-- Leaflet Control Geocoder JS -->
<script src="https://unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.js"></script>
<!-- Turf.js for boolean operations and area calculations -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/Turf.js/6.5.0/turf.min.js"></script>
// Initialize the map centered at Syntagma Square, Athens, Greece at zoom level 18.
var map = L.map('map').setView([37.975, 23.734], 18);
// ESRI Satellite imagery with labels.
// Note: Using maxNativeZoom: 18 lets us scale the tiles beyond that (up to maxZoom: 20)
var satellite = L.tileLayer(
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: 'Tiles © Esri, Earthstar Geographics'
// To add labels over the satellite view, we add a separate tile layer.
var esriLabels = L.tileLayer(
'https://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}', {
attribution: 'Labels © Esri'
// Group the ESRI layers so they act as one base layer.
var esriSatellite = L.layerGroup([satellite, esriLabels]);
// OpenMapTiles base layer.
// This URL is provided for demonstration. In practice, you may want to host your own tiles.
var openMapTiles = L.tileLayer(
'https://tiles.openmaptiles.org/osm-bright/{z}/{x}/{y}.png', {
attribution: '© OpenMapTiles & OpenStreetMap contributors'
// Add ESRI Satellite as the default base layer.
esriSatellite.addTo(map);
// Create a baseMaps object for the layer control.
"Satellite (ESRI)": esriSatellite,
"OpenMapTiles": openMapTiles
// Add the layer control to allow switching between base maps.
L.control.layers(baseMaps).addTo(map);
// --- Additional Controls & Layers ---
// Add a geocoder control (using Nominatim behind the scenes)
defaultMarkGeocode: false
.on('markgeocode', function(e) {
var bbox = e.geocode.bbox;
map.fitBounds(poly.getBounds());
// Layer group to hold drawn shapes
var drawnItems = new L.FeatureGroup();
map.addLayer(drawnItems);
// Initialize the drawing control (only polygons)
var drawControl = new L.Control.Draw({
allowIntersection: false, // avoid self-intersections
edit: { featureGroup: drawnItems }
map.addControl(drawControl);
// Array to track selected layers for boolean operations
// Utility functions to highlight/unhighlight shapes
function highlight(layer) {
layer.setStyle({ color: 'red' });
function resetHighlight(layer) {
layer.setStyle({ color: 'blue' });
// When a new polygon is drawn, add a click listener to toggle its selection
map.on(L.Draw.Event.CREATED, function (event) {
drawnItems.addLayer(layer);
layer.setStyle({ color: 'blue' });
// Add a click event for selection
layer.on('click', function(e) {
L.DomEvent.stopPropagation(e);
if (selectedLayers.includes(layer)) {
selectedLayers = selectedLayers.filter(function(l) { return l !== layer; });
selectedLayers.push(layer);
// Optionally, bind a popup to show area upon creation
if (layer instanceof L.Polygon) {
var geojson = layer.toGeoJSON();
var area = turf.area(geojson);
var areaStr = area.toFixed(2) + ' m²';
layer.bindPopup('Area: ' + areaStr);
// Create a custom control with buttons for boolean operations
var BooleanControl = L.Control.extend({
options: { position: 'topright' },
var container = L.DomUtil.create('div', 'custom-control');
<button id="union-btn">Add (Union)</button>
<button id="subtract-btn">Subtract (Difference)</button>
<button id="clear-selection-btn">Clear Selection</button>
L.DomEvent.disableClickPropagation(container);
map.addControl(new BooleanControl());
// Button: Union (add shapes)
document.getElementById('union-btn').addEventListener('click', function() {
if (selectedLayers.length !== 2) {
alert('Please select exactly two shapes for the union operation.');
var poly1 = selectedLayers[0].toGeoJSON();
var poly2 = selectedLayers[1].toGeoJSON();
var unionPoly = turf.union(poly1, poly2);
alert('Union operation failed.');
drawnItems.removeLayer(selectedLayers[0]);
drawnItems.removeLayer(selectedLayers[1]);
var newLayer = L.geoJSON(unionPoly, { style: { color: 'blue' } }).getLayers()[0];
newLayer.on('click', function(e) {
L.DomEvent.stopPropagation(e);
if (selectedLayers.includes(newLayer)) {
selectedLayers = selectedLayers.filter(function(l) { return l !== newLayer; });
resetHighlight(newLayer);
selectedLayers.push(newLayer);
drawnItems.addLayer(newLayer);
newLayer.bindPopup('Union of selected shapes');
// Button: Subtract (difference)
document.getElementById('subtract-btn').addEventListener('click', function() {
if (selectedLayers.length !== 2) {
alert('Please select exactly two shapes for the subtraction operation. The second shape will be subtracted from the first.');
var poly1 = selectedLayers[0].toGeoJSON();
var poly2 = selectedLayers[1].toGeoJSON();
var diffPoly = turf.difference(poly1, poly2);
alert('Subtraction operation resulted in no area or failed.');
drawnItems.removeLayer(selectedLayers[0]);
drawnItems.removeLayer(selectedLayers[1]);
var newLayer = L.geoJSON(diffPoly, { style: { color: 'blue' } }).getLayers()[0];
newLayer.on('click', function(e) {
L.DomEvent.stopPropagation(e);
if (selectedLayers.includes(newLayer)) {
selectedLayers = selectedLayers.filter(function(l) { return l !== newLayer; });
resetHighlight(newLayer);
selectedLayers.push(newLayer);
drawnItems.addLayer(newLayer);
newLayer.bindPopup('Result: First shape minus second');
// Button: Clear Selection
document.getElementById('clear-selection-btn').addEventListener('click', function() {
selectedLayers.forEach(function(layer) {
// Clear selections if clicking on the map background
map.on('click', function() {
selectedLayers.forEach(function(layer) {