Leaflet Map: Adding Drawing Functionality

2024-02-10

In this post, I'll share my journey of implementing drawing functionality on a Leaflet map, from initial research to overcoming challenges and finally achieving success. Drawing from my experience working on the Geo Game App project here: https://chimerical-meringue-763758.netlify.app/, where I used Leaflet Map, I felt ready to tackle this task.

Research and Preparation:

My journey began with thorough research, diving into Leaflet's documentation and scouring the web for examples and tutorials. Armed with this knowledge, I was ready to embark on the implementation process.

Implementation:

With a clear understanding of Leaflet's capabilities, I began by integrating drawing functionality into my map. Leveraging Leaflet's built-in drawing tools and exploring plugins like Leaflet.Draw, I crafted a user-friendly interface that allowed users to draw polygons directly onto the map canvas.

Here's an example:

import React, { Component } from 'react';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import 'leaflet-draw/dist/leaflet.draw.css';
import 'leaflet-draw';
import { csrfToken } from './utils/csrfToken'; // Import CSRF token

class LeafletMap extends Component {
  constructor(props) {
    super(props);
    this.mapRef = React.createRef();
    this.drawnItems = new L.FeatureGroup();
    this.drawControl = null;
    this.state = {
      isDrawing: false
    };
  }

  componentDidMount() {
    this.map = L.map(this.mapRef.current).setView([51.505, -0.09], 13);

    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
      attribution: '© OpenStreetMap contributors'
    }).addTo(this.map);

    this.map.addLayer(this.drawnItems);
  }

  initDrawControl = () => {
    if (!this.drawControl) {
      this.drawControl = new L.Control.Draw({
        draw: {
          polygon: true,
          polyline: false,
          rectangle: false,
          circle: false,
          marker: false
        },
        edit: {
          featureGroup: this.drawnItems
        }
      });
      this.map.addControl(this.drawControl);
      this.map.on('draw:created', this.onDrawCreated);
    }
  };

  onDrawCreated = (e) => {
    const layer = e.layer;
    this.drawnItems.addLayer(layer);
    this.setState({ isDrawing: false });

    // Show buttons for done, cancel, and undo
    const toolbar = this.drawControl._toolbars.draw;
    toolbar._modes.polygon.handler.enable();
    toolbar._modes.polygon.handler.disable(); // Disable drawing after polygon is created

    toolbar._modes.remove.handler.enable(); // Enable remove (undo) button
    toolbar._modes.remove.handler.on('click', () => {
      const layers = this.drawnItems.getLayers();
      if (layers.length > 0) {
        const lastLayer = layers[layers.length - 1];
        this.drawnItems.removeLayer(lastLayer);
      }
    });

    toolbar._modes.cancel.handler.enable(); // Enable cancel button
    toolbar._modes.cancel.handler.on('click', () => {
      this.drawnItems.clearLayers(); // Remove all drawn layers
      toolbar._modes.polygon.handler.disable(); // Disable drawing
    });

    toolbar._modes.edit.handler.enable(); // Enable done (save) button
    toolbar._modes.edit.handler.on('click', () => {
      // Convert drawn polygon to GeoJSON format
      const geojson = layer.toGeoJSON();

      // Send GeoJSON data to server using Django's CSRF protection and URL
      fetch('/savePolygon/', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-CSRFToken': csrfToken // Pass the CSRF token in the headers
        },
        body: JSON.stringify({ data: JSON.stringify(geojson) })
      })
        .then(response => {
          if (response.ok) {
            console.log('Polygon saved successfully!');
          } else {
            console.error('Failed to save polygon:', response.statusText);
          }
        })
        .catch(error => {
          console.error('Failed to save polygon:', error);
        });
    });
  };

  render() {
    return (
      <div>
        <div ref={this.mapRef} style={{ height: '400px' }}></div>
        {!this.state.isDrawing && (
          <button onClick={() => {
            this.setState({ isDrawing: true });
            this.initDrawControl();
          }}>Draw</button>
        )}
        {this.state.isDrawing && <p>Please draw on the map</p>}
      </div>
    );
  }
}

export default LeafletMap;

What this code does:

  1. Component Structure and State: We define a class component called LeafletMap that extends Component. This component represents the Leaflet map with drawing functionality. Inside the constructor, we initialize the component's state with isDrawing set to false. This state variable tracks whether the user is currently drawing on the map.

  2. Map Initialization: In the componentDidMount lifecycle method, we initialize the Leaflet map by creating a map instance and adding a tile layer for OpenStreetMap.

  3. Drawing Control Initialization: The initDrawControl method initializes the drawing control for the map. It creates a new instance of L.Control.Draw with options to enable drawing polygons and disable other drawing modes like polyline, rectangle, circle, and marker. This method is called when the user clicks the "Draw" button.

  4. Drawing Polygon: When the user draws a polygon on the map, the onDrawCreated event handler is triggered. Inside this handler: We add the drawn polygon layer to the drawnItems feature group, which displays the drawn polygons on the map. We disable the drawing handler for polygons to prevent further drawing. We enable the remove (undo), cancel, and edit (done/save) buttons in the drawing toolbar.

  5. Button Click Handlers: Clicking the "Remove" button (undo) removes the last drawn polygon from the map. Clicking the "Cancel" button clears all drawn polygons from the map and disables further drawing. Clicking the "Edit" button (done/save) converts the drawn polygon to GeoJSON format, sends it to the server via an AJAX request, and saves it to the database using Django's CSRF protection and URL.

  6. Rendering: The render method renders the Leaflet map and a "Draw" button when isDrawing is false. When isDrawing is true, it renders the "Please draw on the map" text below the map.

Conclusion:

In conclusion, I found the process of implementing the drawing feature incredibly fulfilling. The improvements in user experience (UX) and user interface (UI) were quite satisfying. With clearer guidance provided to users, they can now navigate the drawing process with ease and confidence. It's empowering to see users have full control over the shapes they draw and the ability to save them directly onto their map.

Moreover, the collaborative aspect of reviewing and learning from the code together has been fantastic. It's not just about writing code; it's about growing and evolving as developers through shared experiences. This journey has been both enjoyable and enlightening, and I look forward to more opportunities for collaborative learning in the future.


Also on codewithkatyrosli.com

Exploring Django Views: A Beginner's Guide to Managing User Profiles, 2024-04-21

Recently, I have been coding in backend Django Python, working on a project related to auctions for forest wood. In this project, users can place bids to sell their forest wood. Today, I would like to share my experience with Django and how views function in a Django application. Specifically, we'll explore a simple example of a Django view that manages a user profile and allows users to view and edit their own profile information.

Read More

This website uses cookies to enchance the user experience.