Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can one make an ellipse in react-leaflet?

I am trying to draw an ellipse on a map made using react-leaflet, which has built-in support for circles and rectangles.

To achieve this, I am using code to produce an ellipse in (non-react) leaflet from here, that I have adapted and pasted below:

import * as L from 'leaflet';


L.SVG.include ({
    _updateEllipse: function (layer) {
        var // c = layer._point,
            rx = layer._radiusX,
            ry = layer._radiusY,
            phi = layer._tiltDeg,
            endPoint = layer._endPointParams;

        var d = 'M' + endPoint.x0 + ',' + endPoint.y0 +
            'A' + rx + ',' + ry + ',' + phi + ',' +
            endPoint.largeArc + ',' + endPoint.sweep + ',' +
            endPoint.x1 + ',' + endPoint.y1 + ' z';
        this._setPath(layer, d);
    }
});

L.Canvas.include ({
    _updateEllipse: function (layer) {
        if (layer._empty()) { return; }

        var p = layer._point,
            ctx = this._ctx,
            r = layer._radiusX,
            s = (layer._radiusY || r) / r;

        this._drawnLayers[layer._leaflet_id] = layer;

        ctx.save();

        ctx.translate(p.x, p.y);
        if (layer._tilt !== 0) {
            ctx.rotate( layer._tilt );
        }
        if (s !== 1) {
            ctx.scale(1, s);
        }

        ctx.beginPath();
        ctx.arc(0, 0, r, 0, Math.PI * 2);
        ctx.restore();

        this._fillStroke(ctx, layer);
    },
});

L.Ellipse = L.Path.extend({

    options: {
        fill: true,
        startAngle: 0,
        endAngle: 359.9
    },

    initialize: function (latlng, radii, tilt, options) {

        L.setOptions(this, options);
        this._latlng = L.latLng(latlng);

        if (tilt) {
            this._tiltDeg = tilt;
        } else {
            this._tiltDeg = 0;
        }

        if (radii) {
            this._mRadiusX = radii[0];
            this._mRadiusY = radii[1];
        }
    },

    setRadius: function (radii) {
        this._mRadiusX = radii[0];
        this._mRadiusY = radii[1];
        return this.redraw();
    },

    getRadius: function () {
        return new L.point(this._mRadiusX, this._mRadiusY);
    },

    setTilt: function (tilt) {
        this._tiltDeg = tilt;
        return this.redraw();
    },

    getBounds: function () {
        // TODO respect tilt (bounds are too big)
        var lngRadius = this._getLngRadius(),
            latRadius = this._getLatRadius(),
            latlng = this._latlng;

        return new L.LatLngBounds(
            [latlng.lat - latRadius, latlng.lng - lngRadius],
            [latlng.lat + latRadius, latlng.lng + lngRadius]);
    },

    // @method setLatLng(latLng: LatLng): this
    // Sets the position of a circle marker to a new location.
    setLatLng: function (latlng) {
        this._latlng = L.latLng(latlng);
        this.redraw();
        return this.fire('move', {latlng: this._latlng});
    },

    // @method getLatLng(): LatLng
    // Returns the current geographical position of the circle marker
    getLatLng: function () {
        return this._latlng;
    },

    setStyle: L.Path.prototype.setStyle,

    _project: function () {
        var lngRadius = this._getLngRadius(),
            latRadius = this._getLatRadius(),
            latlng = this._latlng,
            pointLeft = this._map.latLngToLayerPoint([latlng.lat, latlng.lng - lngRadius]),
            pointBelow = this._map.latLngToLayerPoint([latlng.lat - latRadius, latlng.lng]);

        this._point = this._map.latLngToLayerPoint(latlng);
        this._radiusX = Math.max(this._point.x - pointLeft.x, 1);
        this._radiusY = Math.max(pointBelow.y - this._point.y, 1);
        this._tilt = Math.PI * this._tiltDeg / 180;
        this._endPointParams = this._centerPointToEndPoint();
        this._updateBounds();
    },

    _updateBounds: function () {
        // http://math.stackexchange.com/questions/91132/how-to-get-the-limits-of-rotated-ellipse
        var sin = Math.sin(this._tilt);
        var cos = Math.cos(this._tilt);
        var sinSquare = sin * sin;
        var cosSquare = cos * cos;
        var aSquare = this._radiusX * this._radiusX;
        var bSquare = this._radiusY * this._radiusY;
        var halfWidth = Math.sqrt(aSquare*cosSquare+bSquare*sinSquare);
        var halfHeight = Math.sqrt(aSquare*sinSquare+bSquare*cosSquare);
        var w = this._clickTolerance();
        var p = [halfWidth + w, halfHeight + w];
        this._pxBounds = new L.Bounds(this._point.subtract(p), this._point.add(p));
    },

    _update: function () {
        if (this._map) {
            this._updatePath();
        }
    },

    _updatePath: function () {
        this._renderer._updateEllipse(this);
    },

    _getLatRadius: function () {
        return (this._mRadiusY / 40075017) * 360;
    },

    _getLngRadius: function () {
        return ((this._mRadiusX / 40075017) * 360) / Math.cos((Math.PI / 180) * this._latlng.lat);
    },

    _centerPointToEndPoint: function () {
        // Convert between center point parameterization of an ellipse
        // too SVG's end-point and sweep parameters.  This is an
        // adaptation of the perl code found here:
        // http://commons.oreilly.com/wiki/index.php/SVG_Essentials/Paths
        var c = this._point,
            rx = this._radiusX,
            ry = this._radiusY,
            theta2 = (this.options.startAngle + this.options.endAngle) * (Math.PI / 180),
            theta1 = this.options.startAngle * (Math.PI / 180),
            delta = this.options.endAngle,
            phi = this._tiltDeg * (Math.PI / 180);

        // Determine start and end-point coordinates
        var x0 = c.x + Math.cos(phi) * rx * Math.cos(theta1) +
            Math.sin(-phi) * ry * Math.sin(theta1);
        var y0 = c.y + Math.sin(phi) * rx * Math.cos(theta1) +
            Math.cos(phi) * ry * Math.sin(theta1);

        var x1 = c.x + Math.cos(phi) * rx * Math.cos(theta2) +
            Math.sin(-phi) * ry * Math.sin(theta2);
        var y1 = c.y + Math.sin(phi) * rx * Math.cos(theta2) +
            Math.cos(phi) * ry * Math.sin(theta2);

        var largeArc = (delta > 180) ? 1 : 0;
        var sweep = (delta > 0) ? 1 : 0;

        return {'x0': x0, 'y0': y0, 'tilt': phi, 'largeArc': largeArc,
            'sweep': sweep, 'x1': x1, 'y1': y1};
    },

    _empty: function () {
        return this._radiusX && this._radiusY && !this._renderer._bounds.intersects(this._pxBounds);
    },

    _containsPoint : function (p) {
        // http://stackoverflow.com/questions/7946187/point-and-ellipse-rotated-position-test-algorithm
        var sin = Math.sin(this._tilt);
        var cos = Math.cos(this._tilt);
        var dx = p.x - this._point.x;
        var dy = p.y - this._point.y;
        var sumA = cos * dx + sin * dy;
        var sumB = sin * dx - cos * dy;
        return sumA * sumA / (this._radiusX * this._radiusX)  + sumB * sumB / (this._radiusY * this._radiusY) <= 1;
    }
});

export const lellipse = function (latlng, radii, tilt, options) {
    return new L.Ellipse(latlng, radii, tilt, options);
};

To create an ellipse to use with react-leaflet, I followed the example of Circle in react-leaflet to produce the following Ellipse component:

import PropTypes from 'prop-types'

import { lellipse as LeafletEllipse } from '../l.ellipse';
import Path from './Path'
import children from './propTypes/children'
import latlng from './propTypes/latlng'
import type { LatLng, MapLayerProps, PathOptions } from './types'

type LeafletElement = LeafletEllipse
type Props = {
  center: LatLng,
  mSemiMajorAxis: number,
  mSemiMinorAxis: number,
  degreeTiltFromWest: number,
} & MapLayerProps &
  PathOptions &
  Object

export default class Ellipse extends Path<LeafletElement, Props> {
  static propTypes = {
    center: latlng.isRequired,
    mSemiMajorAxis: PropTypes.number.isRequired,
    mSemiMinorAxis: PropTypes.number.isRequired,
    degreeTiltFromWest: PropTypes.number.isRequired,
    children: children,
  }

  createLeafletElement(props: Props): LeafletElement {
    const { center, mSemiMajorAxis, mSemiMinorAxis, degreeTiltFromWest, ...options } = props
    return new LeafletEllipse(center, [mSemiMajorAxis, mSemiMinorAxis], this.getOptions(options))
  }

  updateLeafletElement(fromProps: Props, toProps: Props) {
    if (toProps.center !== fromProps.center) {
      this.leafletElement.setLatLng(toProps.center);
    }
    if (toProps.degreeTiltFromWest !== fromProps.degreeTiltFromWest) {
      this.leafletElement.setTilt(toProps.degreeTiltFromWest);
    }
    if (toProps.mSemiMinorAxis !== fromProps.mSemiMinorAxis || toProps.mSemiMajorAxis !== fromProps.mSemiMajorAxis) {
      this.leafletElement.setRadius([toProps.mSemiMajorAxis, toProps.mSemiMinorAxis]);
    }

  }
}

The problem with the code is that it does not render an ellipse and it does not throw any errors. Could someone suggest how to render an ellipse with react-leaflet? Thanks.

like image 477
Shafique Jamal Avatar asked Mar 03 '18 20:03

Shafique Jamal


People also ask

How do you make an ellipse in React JS?

In React Native we can set the default Ellipsis using numberOfLines = { 1 } prop of Text component. This prop would allow us to implement the Ellipsis Clipped effect on Text.

Can you use leaflet with React?

Leaflet and its React counterpart, React Leaflet, are a fantastic open source and free mapping alternative to Google Maps and MapBox, no API key required! It is an easy package to work with and one worth trying out.

How do you trigger an event in React?

To add the click event in React using plain JavaScript, you need to use addEventListener() to assign the click event to an element. Create one <button> element as ref props so that it can be accessed to trigger the click event.


1 Answers

Your createLeafletElement function is missing the tilt parameter. It should be:

   createLeafletElement(props) {
        const { center, mSemiMajorAxis, mSemiMinorAxis, degreeTiltFromWest, ...options } = props
        return new LeafletEllipse(center, [mSemiMajorAxis, mSemiMinorAxis], degreeTiltFromWest, this.getOptions(options))
    }

See below for the complete file (in ES6 rather than in typescript, as I find it clearer).

import React, { PropTypes } from 'react';
import { lellipse as LeafletEllipse } from './l.ellipse';
import { Path, withLeaflet } from 'react-leaflet';

class Ellipse extends Path {

    static propTypes = {
        center: PropTypes.arrayOf(PropTypes.number).isRequired,
        mSemiMajorAxis: PropTypes.number.isRequired,
        mSemiMinorAxis: PropTypes.number.isRequired,
        degreeTiltFromWest: PropTypes.number.isRequired
    }

    createLeafletElement(props) {
        const { center, mSemiMajorAxis, mSemiMinorAxis, degreeTiltFromWest, ...options } = props
        return new LeafletEllipse(center, [mSemiMajorAxis, mSemiMinorAxis], degreeTiltFromWest, this.getOptions(options))
    }

    updateLeafletElement(fromProps, toProps) {
        if (toProps.center !== fromProps.center) {
            this.leafletElement.setLatLng(toProps.center);
        }
        if (toProps.degreeTiltFromWest !== fromProps.degreeTiltFromWest) {
            this.leafletElement.setTilt(toProps.degreeTiltFromWest);
        }
        if (toProps.mSemiMinorAxis !== fromProps.mSemiMinorAxis || toProps.mSemiMajorAxis !== fromProps.mSemiMajorAxis) {
            this.leafletElement.setRadius([toProps.mSemiMajorAxis, toProps.mSemiMinorAxis]);
        }
    }
} 
export default class withLeaflet(Ellipse);
like image 82
grascioni Avatar answered Oct 19 '22 11:10

grascioni