Can Someone Explain Path.SVG to Me?

882 views
Skip to first unread message

FunkMonkey33

unread,
Sep 18, 2012, 7:32:24 PM9/18/12
to leafl...@googlegroups.com
I wrote a pretty good extension to the CircleMarker called LabeledCircleMarker. Basically it just displays an SVG circle with an SVG Text element inside of it:  
 

 
It works quite well against the leaflet source code (0.3.1), but not at all against the distributed file (either leaflet.js or leaflet-src.js). And probably not at all against the latest version either (haven't tried yet).
 
Here is some background. 
 
I needed this functionality for our server-side clustering solution.  Each cluster would be shown as a circle, with the number of locations written inside of the circle (instead of hello world in this screenshot).
 
I needed to redefine L.Circle to not draw itself using an SVG path.  In SVG it's a pretty trivial thing to put a text element in front of a circle element, but if you put it in front of a path element (even a path that draws a circle), it doesn't work. 
 
So I created LabeledCircle, which inherits from Circle (adding options for the label and label style), LabeledCircleMarker, which inherits from LabeledCircle (and just essentially replaces the code for CircleMarker, inheriting from LabeledCircle instead of Circle), and then I created LabeledCircleSvgPath, which overrides some of the methods in Path.SVG, and adds some methods as well.
 
I think it works against the leaflet source code, because LabeledCircleSvgPath is getting loaded after Path.SVG, and is replacing some of the methods that are defined in Path.SVG with the redefinitions in LabeledCircleSvgPath.  But, when I run it against the compiled Leaflet code, it's not able to manage those redifinitions.
 
I'll attach a zip file of the solution so you can see what I'm talking about, but basically I don't really understand the inheritance structure of Path.SVG.  It looks like Path is extending itself, which seems kind of strange (of course I'm fairly new to using JavaScript at this level). 
 
Just unzip it, then launch index.htm, and say yes to the javaScript warning if you get one.
 
By the way, if you're wondering where I got all the script references in the head of my HTML, I basically used the deps.js file in the build directory to tell me what I needed to load.  It helped me in stepping through the Leaflet source code, to have all the files be separate. 
 
Thanks in advance.  Looking forward to sharing this with the Leaflet community when it's done.
 
Aaron
LabeledCircleMarkerTest.zip

FunkMonkey33

unread,
Sep 24, 2012, 7:31:54 PM9/24/12
to leafl...@googlegroups.com
I figured this out. 
 
Basically I needed to make sure Path.js was not used.  Path.Svg inherited from Path, and so did my code, and it basically came down to what was getting loaded first. 
 
So I wrote 1 Javascript file which incorporates all of the functionaliy and all of the classes that we need.  Just load the Leaflet script first (leaflet.js works, as does leaflet-src.js), then load this 1 additional file:
 

<script type="text/javascript" src="Leaflet/dist/leaflet.js"></script>

<script type="text/javascript" src="LabeledCircleMarker.js"></script>

It works with the new version of Leaflet as well.

Code is attached as a text file.  Just change the suffix to .js and you should be good to go.  Please see my MapScripts.js file in the previous post (zipped up with the rest of the solution) for an example of how to use this extension.

Hope this helps someone!

Aaron (FunkMonkey33)

 

FunkMonkey33

unread,
Sep 24, 2012, 7:34:35 PM9/24/12
to leafl...@googlegroups.com
Here is the attachment:

 

FunkMonkey33

unread,
Sep 24, 2012, 7:36:38 PM9/24/12
to leafl...@googlegroups.com
Okay, the attachment isn't coming through, so here it is inline: 
 
L.Path.SVG_NS = 'http://www.w3.org/2000/svg';
L.Browser.svg = !!(document.createElementNS && document.createElementNS(L.Path.SVG_NS, 'svg').createSVGRect);
L.SvgPath = L.Class.extend({
    // first all the Path Stuff
    includes: [L.Mixin.Events],
 statics: {
  CLIP_PADDING: 0.5,
  SVG: L.Browser.svg,
  _createElement: function (name) {
      return document.createElementNS(L.Path.SVG_NS, name);
  }
    },
 options: {
  stroke: true,
  color: '#0033ff',
  weight: 5,
  opacity: 0.5,
  fill: false,
  fillColor: null, //same as color by default
  fillOpacity: 0.2,
  clickable: true,
  // TODO remove this, as all paths now update on moveend
  updateOnMoveEnd: true
 },
 initialize: function (options) {
  L.Util.setOptions(this, options);
 },
 onAdd: function (map) {
  this._map = map;
  this._initElements();
  this._initEvents();
  this.projectLatlngs();
  this._updatePath();
  map.on('viewreset', this.projectLatlngs, this);
  this._updateTrigger = this.options.updateOnMoveEnd ? 'moveend' : 'viewreset';
  map.on(this._updateTrigger, this._updatePath, this);
 },
 onRemove: function (map) {
  this._map = null;
  map._pathRoot.removeChild(this._container);
  map.off('viewreset', this.projectLatlngs, this);
  map.off(this._updateTrigger, this._updatePath, this);
 },
 projectLatlngs: function () {
  // do all projection stuff here
 },
 setStyle: function (style) {
  L.Util.setOptions(this, style);
  if (this._container) {
   this._updateStyle();
  }
  return this;
 },
 _redraw: function () {
  if (this._map) {
   this.projectLatlngs();
   this._updatePath();
  }
 },
    // now all the non-overridden methods from Path.SVG
    getPathString: function () {
        // form path string here
    },
    _initElements: function () {
        this._map._initPathRoot();
        this._initPath();
        this._initStyle();
    },
    _initPath: function () {
        this._container = L.SvgPath._createElement('g');
        this._path = L.SvgPath._createElement('circle');
        this._label = L.SvgPath._createElement('text');
        this._mask = L.SvgPath._createElement('rect');
        this._container.appendChild(this._path);
        this._container.appendChild(this._label);
        this._container.appendChild(this._mask);
        this._map._pathRoot.appendChild(this._container);
    },
    _initStyle: function () {
        if (this.options.stroke) {
            this._path.setAttribute('stroke-linejoin', 'round');
            this._path.setAttribute('stroke-linecap', 'round');
        }
        if (this.options.fill) {
            this._path.setAttribute('fill-rule', 'evenodd');
        } else {
            this._path.setAttribute('fill', 'none');
        }
        this._updateStyle();
    },
    _updateStyle: function () {
        if (this.options.stroke) {
            this._path.setAttribute('stroke', this.options.color);
            this._path.setAttribute('stroke-opacity', this.options.opacity);
            this._path.setAttribute('stroke-width', this.options.weight);
        }
        if (this.options.fill) {
            this._path.setAttribute('fill', this.options.fillColor || this.options.color);
            this._path.setAttribute('fill-opacity', this.options.fillOpacity);
        }
        if (this.options.radius) {
            this._path.setAttribute('r', this.options.radius);
        }
        if (this.options.label) {
            this._label.textContent = this.options.label;
            this._label.setAttribute("text-anchor", this.options.textAnchor);
            this._label.setAttribute("font-family", this.options.fontFamily);
            this._label.setAttribute("font-size", this.options.fontSize + "px");
            this._label.setAttribute("font-weight", this.options.fontWeight);
            this._label.setAttribute("stroke", this.options.labelStrokeColor);
            this._label.setAttribute("fill", this.options.labelFillColor);
        }
    },
    _updatePath: function () {
        if (this._point) {
            this._path.setAttribute("cx", this._point.x);
            this._path.setAttribute("cy", this._point.y);
        }
        if (this._label) {
            // set label's position
            var y = this._point.y + (this.options.fontSize / 4) + this.options.yOffset;
            this._label.setAttribute("x", this._point.x + this.options.xOffset);
            this._label.setAttribute("y", y);
            // set mask's position
            var maskX = this._point.x - this.options.radius;
            var maskY = this._point.y - this.options.radius;
            this._mask.setAttribute("x", maskX);
            this._mask.setAttribute("y", maskY);
            this._mask.setAttribute("width", this.options.radius * 2);
            this._mask.setAttribute("height", this.options.radius * 2);
            this._mask.setAttribute("opacity", 0);
        }
    },
   
    _initEvents: function () {
        if (this.options.clickable) {
            if (!L.Browser.vml) {
                this._path.setAttribute('class', 'leaflet-clickable');
            }
            L.DomEvent.addListener(this._container, 'click', this._onMouseClick, this);
            var events = ['dblclick', 'mousedown', 'mouseover', 'mouseout', 'mousemove'];
            for (var i = 0; i < events.length; i++) {
                L.DomEvent.addListener(this._container, events[i], this._fireMouseEvent, this);
            }
        }
    },
 _onMouseClick: function (e) {
  if (this._map.dragging && this._map.dragging.moved()) {
   return;
  }
  this._fireMouseEvent(e);
 },
 _fireMouseEvent: function (e) {
  if (!this.hasEventListeners(e.type)) {
   return;
  }
  this.fire(e.type, {
   latlng: this._map.mouseEventToLatLng(e),
   layerPoint: this._map.mouseEventToLayerPoint(e)
  });
  L.DomEvent.stopPropagation(e);
 },
    updateLabelText: function (newLabelText) {
        this.options.label = newLabelText;
        this._updateStyle();
    },
    updateCircle: function (circleOptions) {
        L.Util.setOptions(this, circleOptions);
        this._updatePath();
        this._updateStyle();
    }
});
L.Map.include({
    _updatePathViewport: function () {
        var p = L.Path.CLIP_PADDING,
   size = this.getSize(),
        //TODO this._map._getMapPanePos()
   panePos = L.DomUtil.getPosition(this._mapPane),
   min = panePos.multiplyBy(-1).subtract(size.multiplyBy(p)),
   max = min.add(size.multiplyBy(1 + p * 2));
        this._pathViewport = new L.Bounds(min, max);
    }
});
L.LabeledCircle = L.SvgPath.extend({
    initialize: function (latlng, radius, options) {
        L.Path.prototype.initialize.call(this, options);
        this._latlng = latlng;
        this._mRadius = radius;
    },
    options: {
        fill: true,
        label: "hola",
        xOffset: 0,
        yOffset: 0,
        textAnchor: "middle",
        fontFamily: "Arial",
        fontSize: "12",
        fontWeight: "bold",
        labelStrokeColor: "white",
        labelFillColor: "white"
    },
    setLatLng: function (latlng) {
        this._latlng = latlng;
        this._redraw();
        return this;
    },
    setRadius: function (radius) {
        this._mRadius = radius;
        this._redraw();
        return this;
    },
    projectLatlngs: function () {
        var equatorLength = 40075017,
   hLength = equatorLength * Math.cos(L.LatLng.DEG_TO_RAD * this._latlng.lat);
        var lngSpan = (this._mRadius / hLength) * 360,
   latlng2 = new L.LatLng(this._latlng.lat, this._latlng.lng - lngSpan, true),
   point2 = this._map.latLngToLayerPoint(latlng2);
        this._point = this._map.latLngToLayerPoint(this._latlng);
        this._radius = Math.round(this._point.x - point2.x);
    },
    getPathString: function () {
        var p = this._point,
   r = this._radius;
        if (this._checkIfEmpty()) {
            return '';
        }
        if (L.Browser.svg) {
            return "M" + p.x + "," + (p.y - r) +
     "A" + r + "," + r + ",0,1,1," +
     (p.x - 0.1) + "," + (p.y - r) + " z";
        } else {
            p._round();
            r = Math.round(r);
            return "AL " + p.x + "," + p.y + " " + r + "," + r + " 0," + (65535 * 360);
        }
    },
    _checkIfEmpty: function () {
        var vp = this._map._pathViewport,
   r = this._radius,
   p = this._point;
        return p.x - r > vp.max.x || p.y - r > vp.max.y ||
   p.x + r < vp.min.x || p.y + r < vp.min.y;
    }
});
L.LabeledCircleMarker = L.LabeledCircle.extend({
    options: {
        radius: 30,
        weight: 2
    },
    initialize: function (latlng, options) {
        L.LabeledCircle.prototype.initialize.call(this, latlng, null, options);
        this._radius = this.options.radius;
    },
    projectLatlngs: function () {
        this._point = this._map.latLngToLayerPoint(this._latlng);
    },
    setRadius: function (radius) {
        this._radius = radius;
        this._redraw();
        return this;
    }
});

 
Reply all
Reply to author
Forward
0 new messages