Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
DrawTools.js 13.02 KiB
import React, { Component } from "react";
import { EditControl } from "react-leaflet-draw";
import L from "leaflet";
import "leaflet-draw";
import {
  FeatureGroup,
  Circle,
  Marker,
  Polygon,
  Polyline,
  Rectangle,
  Tooltip
} from "react-leaflet";

// an empty icon for textboxes
let noIcon = L.divIcon({
  className: "",
  iconSize: [20, 20],
  iconAnchor: [10, 20]
});

// class for text field
L.Draw.MarkerTextBox = L.Draw.Marker.extend({
  options: {
    icon: noIcon,
    repeatMode: false,
    interactive: true
  },
  initialize: function(map, options) {
    this.type = "textbox"; // important to have a unique type, so that it won't get mixed up with other elements
    this.featureTypeCode = "textbox";
    L.Draw.Feature.prototype.initialize.call(this, map, options);
  }
});

// Overriding default toolbar
// Just adding one new button though lol
L.DrawToolbar.include({
  getModeHandlers: function(map) {
    return [
      {
        enabled: this.options.polyline,
        handler: new L.Draw.Polyline(map, this.options.polyline),
        title: L.drawLocal.draw.toolbar.buttons.polyline
      },
      {
        enabled: this.options.polygon,
        handler: new L.Draw.Polygon(map, this.options.polygon),
        title: L.drawLocal.draw.toolbar.buttons.polygon
      },
      {
        enabled: this.options.rectangle,
        handler: new L.Draw.Rectangle(map, this.options.rectangle),
        title: L.drawLocal.draw.toolbar.buttons.rectangle
      },
      {
        enabled: this.options.circle,
        handler: new L.Draw.Circle(map, this.options.circle),
        title: L.drawLocal.draw.toolbar.buttons.circle
      },
      {
        enabled: this.options.marker,
        handler: new L.Draw.Marker(map, this.options.marker),
        title: L.drawLocal.draw.toolbar.buttons.marker
      },
      {
        enabled: this.options.marker,
        handler: new L.Draw.MarkerTextBox(map, this.options.marker),
        title: "Write text"
      }
    ];
  }
});

class DrawTools extends Component {
  constructor(props) {
    super(props);
    this.state = {
      geoJSONAll: [], // property for all GeoJSON data in the map
      editModeActive: false
    };
  }

  _onCreated = e => {
    // check if a drawn polyline has just one point in it
    if (e.layerType === "polyline" && e.layer.getLatLngs().length === 1) {
      e.layer.remove();
      return;
    }

    if (e.layerType === "textbox") {
      // have to create tooltip as a DOM element to allow text selecting. maybe
      let tooltip = L.DomUtil.create("div", "editable");

      // need ids for tooltips to be able to add a blur event to them
      tooltip.innerHTML =
        '<div contenteditable="true" placeholder="Click out to save" id="' +
        e.layer._leaflet_id +
        '"></div>';

      e.layer.bindTooltip(tooltip, {
        permanent: true,
        direction: "bottom",
        interactive: true
      });

      // disable dragging when cursor is over marker (tooltip)
      // clicking on tooltip fires the marker's click handler, hence e.layer.on
      e.layer.on("mouseover", function() {
        e.layer._map.dragging.disable();
      });

      // enable dragging again when cursor is out of marker (tooltip)
      e.layer.on("mouseout", function() {
        e.layer._map.dragging.enable();
      });

      // show placeholder text again upon emptying textbox
      e.layer.on("keyup", function() {
        // when the text area is emptied, a <br> appears
        // manually removing it so that the placeholder text can show
        if (
          tooltip.innerHTML ===
            '<div placeholder="Click out to save" contenteditable="true" id ="' +
              e.layer._leaflet_id +
              "><br></div>" ||
          tooltip.innerHTML ===
            '<div placeholder="Click out to save" contenteditable="true" id ="' +
              e.layer._leaflet_id +
              "><div><br></div></div>"
        ) {
          tooltip.innerHTML =
            '<div placeholder="Click out to save" contenteditable="true" id ="' +
            e.layer._leaflet_id +
            "></div>";
        }
      });

      // blur event listener can't be given straight to a layer
      // getting element by ID and adding an event listener to the element
      document
        .getElementById(e.layer._leaflet_id)
        .addEventListener(
          "blur",
          this.makeGeoJSON.bind(this, e.layerType, e.layer)
        ); // can't put this.makeGeoJSON(e) straight, as it calls the function
      document.getElementById(e.layer._leaflet_id).focus();

      console.log(e.layer);

      return; // only sending the textbox to database until text has been written
    } // end if (e.layerType === "textbox")

    this.makeGeoJSON(e.layerType, e.layer);
    e.layer.remove();
  }; // end _onCreated

  // turn layer to GeoJSON data
  makeGeoJSON = (layerType, layer) => {
    // setting the format in which the data will be sent
    let geoJSON = {
      data: layer.toGeoJSON(),
      mapDrawingId: layer.options.id,
      drawingIsActive: true
    };

    // setting properties
    if (layerType === "textbox") {
      if (layer._tooltip._content.innerText) {
        geoJSON.data.properties.text = layer._tooltip._content.innerText;
      } else {
        return;
      }
    } else if (layerType === "circle") {
      geoJSON.data.properties.radius = layer._mRadius; // layer.options.radius doesn't update for some reason; need to use _mRadius instead
    } else if (layerType === "rectangle") {
      // rectangle is a simple true/false property to recognize a rectangle
      // so that Polygons with this property can be inserted into map with rectangle functionalities instead of Polygon's
      geoJSON.data.properties.rectangle = true;
    }
    geoJSON.data.properties.color = layer.options.color;

    // send item to database, and receive an ID for the layer
    this.props.sendGeoJSON(geoJSON);
  };

  _onEditDeleteStart = () => {
    this.setState({ editModeActive: true });
  };

  _onEditDeleteStop = () => {
    this.setState({ editModeActive: false });
  };

  _onEdited = e => {
    // layers are saved in a rather curious format. they're not in an array, so need to make an array first
    let editedLayers = e.layers;
    let idsToEdit = [];
    editedLayers.eachLayer(function(layer) {
      idsToEdit.push(layer);
    });

    idsToEdit.map(layer => {
      // checking the contents of the layer to determine its type, as it's not explicitly stated
      if (layer.options.bounds) {
        this.makeGeoJSON("rectangle", layer);
      } else if (layer.options.radius) {
        this.makeGeoJSON("circle", layer);
      } else if (layer.options.text) {
        this.makeGeoJSON("textbox", layer);
      } else {
        this.makeGeoJSON(null, layer);
      }
      return true;
    });
  };

  _onDeleted = e => {
    // layers are saved in a rather curious format. they're not in an array, so need to make an array first
    let deletedLayers = e.layers;
    let idsToDelete = [];
    deletedLayers.eachLayer(function(layer) {
      idsToDelete.push(layer);
    });

    idsToDelete.map(layer => {
      let geoJSON = {
        data: layer.toGeoJSON(),
        mapDrawingId: layer.options.id,
        drawingIsActive: false
      };

      this.props.sendGeoJSON(geoJSON);
      return true;
    });
  };

  shouldComponentUpdate(nextProps, nextState) {
    // disable re-rendering when edit mode is active
    return !this.state.editModeActive;
  }

  render() {
    return (
      // "It's important to wrap EditControl component into FeatureGroup component from react-leaflet.
      // The elements you draw will be added to this FeatureGroup layer,
      // when you hit edit button only items in this layer will be edited."
      <FeatureGroup>
        <EditControl
          position="topright"
          onCreated={this._onCreated}
          onEdited={this._onEdited}
          onEditStart={this._onEditDeleteStart}
          onEditStop={this._onEditDeleteStop}
          onDeleted={this._onDeleted}
          onDeleteStart={this._onEditDeleteStart}
          onDeleteStop={this._onEditDeleteStop}
          draw={{
            circle: {
              repeatMode: false, // allows using the tool again after finishing the previous shape
              shapeOptions: {
                color: "#f9f10c",
                opacity: 1 // affects the outline only. for some reason it wasn't at full opacity, so this is needed for more clarity
              }
            },
            rectangle: {
              repeatMode: false
            },
            polygon: {
              repeatMode: true,
              allowIntersection: false, // Restricts shapes to simple polygons
              drawError: {
                color: "#e1e100", // Color the shape will turn when intersects
                message: "<strong>Oh snap!<strong> you can't draw that!" // Message that will show when intersect
              },
              shapeOptions: {
                color: "#ed2572",
                opacity: 1
              }
            },
            polyline: {
              repeatMode: true,
              shapeOptions: {
                color: "#ed2572",
                opacity: 1
              }
            },
            marker: {
              repeatMode: false
            },
            circlemarker: false
          }}
        />

        {/* iterate through every element fetched from back-end */}
        {this.props.geoJSONLayer.features.map(feature => {
          let id = feature.mapDrawingId;
          let coords = feature.data.geometry.coordinates;
          let type = feature.data.geometry.type;
          let color = feature.data.properties.color;
          let radius = feature.data.properties.radius;
          let text = feature.data.properties.text;
          let rectangle = feature.data.properties.rectangle;

          if (type === "Point") {
            // GeoJSON saves latitude first, not longitude like usual. swapping
            let position = [coords[1], coords[0]];
            if (radius) {
              return (
                // keys are required to be able to edit
                <Circle
                  key={Math.random()}
                  center={position}
                  id={id}
                  radius={radius}
                  color={color}
                />
              );
            } else if (text) {
              return (
                <Marker
                  key={Math.random()}
                  position={position}
                  id={id}
                  color={color}
                  icon={noIcon}
                >
                  <Tooltip
                    direction="bottom"
                    permanent
                    className="editable"
                    interactive={true}
                  >
                    <div className="editable">
                      <div
                        contentEditable="true"
                        placeholder="Click out to save"
                      >
                        {text}
                      </div>
                    </div>
                  </Tooltip>
                </Marker>
              );
            } else {
              // unknown if color changes anything. need to test
              return (
                <Marker
                  key={Math.random()}
                  position={position}
                  id={id}
                  color={color}
                />
              );
            }
          } else if (rectangle) {
            // instead of an array of four coordinates, rectangles only have two corners
            let bounds = coords[0].map(coord => {
              return [coord[1], coord[0]];
            });
            return (
              <Rectangle
                key={Math.random()}
                bounds={bounds}
                id={id}
                color={color}
              />
            );
          } else if (type === "Polygon") {
            // Polygon coordinates are wrapped under a one element array, for some reason
            let positions = coords[0].map(coord => {
              return [coord[1], coord[0]];
            });
            return (
              <Polygon
                key={Math.random()}
                positions={positions}
                id={id}
                color={color}
              />
            );
          } else if (type === "LineString") {
            // Polyline coordinates are a normal array, unlike Polygon
            let positions = coords.map(coord => {
              return [coord[1], coord[0]];
            });
            return (
              <Polyline
                key={Math.random()}
                positions={positions}
                id={id}
                color={color}
              />
            );
          }
          return null;
        })}
      </FeatureGroup>
    );
  }
}

export default DrawTools;