├── .gitignore ├── circle.yml ├── index.js ├── demo ├── css │ └── style.css ├── index.html ├── js │ ├── editable.js │ ├── svg2canvas.js │ └── index.js └── data │ ├── export.svg │ ├── export2.svg │ └── 2.svg ├── test ├── utils.test.js ├── bounds.test.js ├── error.test.js └── schematic.test.js ├── src ├── bounds.js ├── utils.js ├── renderer.js └── schematic.js ├── .eslintrc ├── package.json ├── Readme.md └── dist ├── L.Schematic.min.js └── L.Schematic.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | demo/data/00*.svg 3 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 4 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./src/schematic'); 2 | -------------------------------------------------------------------------------- /demo/css/style.css: -------------------------------------------------------------------------------- 1 | body, html, #image-map { 2 | width: 100%; 3 | height: 100%; 4 | margin: 0; 5 | } 6 | 7 | .leaflet-container { 8 | background: #fff; 9 | } 10 | 11 | .schema-select { 12 | position: absolute; 13 | z-index: 999; 14 | right: 20px; 15 | top: 20px; 16 | padding: 5px; 17 | background: #fff; 18 | } 19 | 20 | 21 | .svg-overlay use { 22 | stroke: #f00; 23 | cursor: pointer; 24 | pointer-events: bounding-box; 25 | } 26 | 27 | .svg-overlay use:hover { 28 | stroke-width: 3; 29 | } 30 | 31 | .schematics-renderer { 32 | } 33 | 34 | -------------------------------------------------------------------------------- /test/utils.test.js: -------------------------------------------------------------------------------- 1 | import tape from 'tape'; 2 | import L from 'leaflet'; 3 | import utils from '../src/utils'; 4 | 5 | tape('SVG utils', (t) => { 6 | 7 | t.test(' - L.DomUtil.isNode', (t) => { 8 | t.ok(L.DomUtil.isNode(document.body), 'document'); 9 | t.ok(L.DomUtil.isNode(L.DomUtil.create('div', 'test')), 'div'); 10 | t.ok(L.DomUtil.isNode(document.createTextNode('text')), 'textNode'); 11 | t.notOk(L.DomUtil.isNode('string'), 'not string'); 12 | t.notOk(L.DomUtil.isNode({a: true}), 'not object'); 13 | 14 | t.end(); 15 | }); 16 | 17 | t.test(' - L.DomUtil.getMatrixString', (t) => { 18 | t.equal(L.DomUtil.getMatrixString(L.point(1, 2), 3), 'matrix(3,0,0,3,1,2)', 'matrix str'); 19 | 20 | t.end(); 21 | }); 22 | 23 | t.test(' - L.SVG.copySVGContents', (t) => { 24 | let svg = L.SVG.create('svg'); 25 | svg.appendChild(L.SVG.create('path')); 26 | let copy = L.SVG.create('svg'); 27 | 28 | L.SVG.copySVGContents(svg, copy); 29 | 30 | t.ok(copy.querySelector('path'), 'copied'); 31 | 32 | t.end(); 33 | }); 34 | 35 | t.end(); 36 | }); 37 | -------------------------------------------------------------------------------- /test/bounds.test.js: -------------------------------------------------------------------------------- 1 | import tape from 'tape'; 2 | import L from 'leaflet'; 3 | import bounds from '../src/bounds'; 4 | 5 | tape('Bounds helpers', (t) => { 6 | 7 | t.test('L.Bounds', (t) => { 8 | 9 | t.test(' .toBBox', (t) => { 10 | t.equals(typeof L.Bounds.prototype.toBBox, 'function', 'method present'); 11 | t.end(); 12 | }); 13 | 14 | t.test(' .scale', (t) => { 15 | const b = new L.Bounds([[0, 0], [1, 1]]); 16 | const scaled = b.scale(2); 17 | t.deepEquals(scaled.toBBox(), [-0.5, -0.5, 1.5, 1.5], 'scaled'); 18 | t.end(); 19 | }); 20 | 21 | t.end(); 22 | }); 23 | 24 | t.test('L.LatLngBounds', (t) => { 25 | 26 | t.test(' .toBBox', (t) => { 27 | t.equals(typeof L.LatLngBounds.prototype.toBBox, 'function', 'method present'); 28 | t.end(); 29 | }); 30 | 31 | t.test(' .scale', (t) => { 32 | const b = new L.LatLngBounds([[0, 0], [1, 1]]); 33 | const scaled = b.scale(2); 34 | t.deepEquals(scaled.toBBox(), [-0.5, -0.5, 1.5, 1.5], 'scaled'); 35 | t.end(); 36 | }); 37 | 38 | t.end(); 39 | }); 40 | 41 | t.end(); 42 | }); 43 | -------------------------------------------------------------------------------- /src/bounds.js: -------------------------------------------------------------------------------- 1 | const L = require('leaflet'); 2 | 3 | /** 4 | * @return {Array.} 5 | */ 6 | L.Bounds.prototype.toBBox = function () { 7 | return [this.min.x, this.min.y, this.max.x, this.max.y]; 8 | }; 9 | 10 | 11 | /** 12 | * @param {Number} value 13 | * @return {L.Bounds} 14 | */ 15 | L.Bounds.prototype.scale = function (value) { 16 | const { max, min } = this; 17 | const deltaX = ((max.x - min.x) / 2) * (value - 1); 18 | const deltaY = ((max.y - min.y) / 2) * (value - 1); 19 | 20 | return new L.Bounds([ 21 | [min.x - deltaX, min.y - deltaY], 22 | [max.x + deltaX, max.y + deltaY] 23 | ]); 24 | }; 25 | 26 | 27 | /** 28 | * @return {Array.} 29 | */ 30 | L.LatLngBounds.prototype.toBBox = function () { 31 | return [this.getWest(), this.getSouth(), this.getEast(), this.getNorth()]; 32 | }; 33 | 34 | 35 | /** 36 | * @param {Number} value 37 | * @return {L.LatLngBounds} 38 | */ 39 | L.LatLngBounds.prototype.scale = function (value) { 40 | const ne = this._northEast; 41 | const sw = this._southWest; 42 | const deltaX = ((ne.lng - sw.lng) / 2) * (value - 1); 43 | const deltaY = ((ne.lat - sw.lat) / 2) * (value - 1); 44 | 45 | return new L.LatLngBounds([ 46 | [sw.lat - deltaY, sw.lng - deltaX], 47 | [ne.lat + deltaY, ne.lng + deltaX] 48 | ]); 49 | }; 50 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 10 | 11 | 12 | Leaflet schematics 13 | 14 | 15 | 16 |
17 |
18 | 30 | 31 |
32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "globals": { 7 | "global": true, 8 | "require": true, 9 | "module": true, 10 | "L": true, 11 | "SVGElementInstance": true 12 | }, 13 | "rules": { 14 | "yoda": 0, 15 | "block-scoped-var": 2, 16 | "camelcase": 1, 17 | "comma-style": 2, 18 | "curly": [ 19 | 2, 20 | "all" 21 | ], 22 | "dot-notation": 0, 23 | "eqeqeq": [ 24 | 2, 25 | "allow-null" 26 | ], 27 | "guard-for-in": 2, 28 | "key-spacing": 0, 29 | "new-cap": 2, 30 | "no-bitwise": 2, 31 | "no-caller": 2, 32 | "no-cond-assign": [ 33 | 2, 34 | "except-parens" 35 | ], 36 | "no-debugger": 2, 37 | "no-empty": 2, 38 | "no-eval": 2, 39 | "no-extend-native": 2, 40 | "no-extra-parens": 0, 41 | "no-irregular-whitespace": 2, 42 | "no-iterator": 2, 43 | "no-loop-func": 2, 44 | "no-multi-spaces": 0, 45 | "no-multi-str": 0, 46 | "no-mixed-spaces-and-tabs": 0, 47 | "no-new": 2, 48 | "no-plusplus": 0, 49 | "no-proto": 2, 50 | "no-script-url": 2, 51 | "no-sequences": 2, 52 | "no-shadow": 2, 53 | "no-undef": 2, 54 | "no-underscore-dangle": 0, 55 | "no-unused-vars": [ 56 | 2, 57 | { 58 | "vars": "all", 59 | "args": "none" 60 | } 61 | ], 62 | "no-use-before-define": [ 63 | 2, 64 | "nofunc" 65 | ], 66 | "no-with": 2, 67 | "quotes": [ 68 | 2, 69 | "single" 70 | ], 71 | "semi": [ 72 | 2, 73 | "always" 74 | ], 75 | "no-extra-semi": 2, // disallow unnecessary semicolons 76 | "semi-spacing": [ 77 | 1, 78 | { 79 | "before": false, 80 | "after": true 81 | } 82 | ], // enforce spacing before and after semicolons 83 | "strict": 0, 84 | "valid-typeof": 2, 85 | "wrap-iife": [ 86 | 2, 87 | "inside" 88 | ] 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leaflet-schematic", 3 | "version": "1.1.0", 4 | "description": "Leaflet SVG viewer for non-cartographic high-detailed schematics", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "browserify -v test/*.test.js | tape-run --render='tap-spec'", 8 | "test:watch": "nodemon --exec 'npm run test'", 9 | "start": "watchify -v --external leaflet -t [ browserify-shim ] -d demo/js/index.js -o demo/js/build.js & npm run server", 10 | "server": "http-server -p 3001", 11 | "lint": "eslint ./src/", 12 | "build-demo": "browserify -v -s L.Schematic ./demo/js/index.js -o demo/js/build.js", 13 | "build-js": "browserify -v -s L.Schematic -t [ browserify-shim ] --external leaflet ./index.js -o dist/L.Schematic.js", 14 | "compress": "uglifyjs dist/L.Schematic.js -o dist/L.Schematic.min.js -m --comments", 15 | "build": "npm run build-demo && npm run lint && npm run build-js && npm run compress" 16 | }, 17 | "author": "Alexander Milevski ", 18 | "license": "MIT", 19 | "dependencies": { 20 | "Base64": "^1.0.0" 21 | }, 22 | "browserify": { 23 | "transform": [ 24 | [ 25 | "babelify", 26 | { 27 | "presets": [ 28 | "es2015" 29 | ] 30 | } 31 | ] 32 | ] 33 | }, 34 | "browserify-shim": { 35 | "leaflet": "global:L" 36 | }, 37 | "devDependencies": { 38 | "babel-preset-es2015": "^6.14.0", 39 | "babelify": "^7.3.0", 40 | "browser-filesaver": "^1.1.1", 41 | "browserify": "^13.1.0", 42 | "browserify-shim": "^3.8.12", 43 | "eslint": "^6.8.0", 44 | "faucet": "0.0.1", 45 | "http-server": "^0.12.1", 46 | "leaflet": "^1.6.0", 47 | "nodemon": "^1.10.2", 48 | "tap-spec": "^5.0.0", 49 | "tape": "^4.6.0", 50 | "tape-run": "^7.0.0", 51 | "uglify-js": "^3.9.1", 52 | "watchify": "^3.7.0", 53 | "xhr": "^2.2.2" 54 | }, 55 | "repository": { 56 | "type": "git", 57 | "url": "https://github.com/w8r/leaflet-schematic.git" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /demo/js/editable.js: -------------------------------------------------------------------------------- 1 | var L = require('leaflet'); 2 | 3 | L.EditControl = L.Control.extend({ 4 | 5 | options: { 6 | position: 'topleft', 7 | callback: null, 8 | renderer: null, 9 | kind: '', 10 | html: '' 11 | }, 12 | 13 | onAdd: function (map) { 14 | var container = L.DomUtil.create('div', 'leaflet-control leaflet-bar'), 15 | link = L.DomUtil.create('a', '', container); 16 | var editTools = map.editTools; 17 | 18 | link.href = '#'; 19 | link.title = 'Create a new ' + this.options.kind; 20 | link.innerHTML = this.options.html; 21 | L.DomEvent 22 | .on(link, 'click', L.DomEvent.stop) 23 | .on(link, 'click', function () { 24 | console.log(editTools); 25 | window.LAYER = editTools[this.options.callback].call(editTools, null, { 26 | renderer: this.options.renderer 27 | }); 28 | }, this); 29 | 30 | return container; 31 | } 32 | 33 | }); 34 | 35 | 36 | L.NewLineControl = L.EditControl.extend({ 37 | options: { 38 | position: 'topleft', 39 | callback: 'startPolyline', 40 | kind: 'line', 41 | html: '\\/\\' 42 | } 43 | }); 44 | 45 | 46 | L.NewPolygonControl = L.EditControl.extend({ 47 | options: { 48 | position: 'topleft', 49 | callback: 'startPolygon', 50 | kind: 'polygon', 51 | html: '▰' 52 | } 53 | }); 54 | 55 | L.NewMarkerControl = L.EditControl.extend({ 56 | options: { 57 | position: 'topleft', 58 | callback: 'startMarker', 59 | kind: 'marker', 60 | html: '🖈' 61 | } 62 | 63 | }); 64 | 65 | L.NewRectangleControl = L.EditControl.extend({ 66 | options: { 67 | position: 'topleft', 68 | callback: 'startRectangle', 69 | kind: 'rectangle', 70 | html: '⬛' 71 | } 72 | }); 73 | 74 | L.NewCircleControl = L.EditControl.extend({ 75 | options: { 76 | position: 'topleft', 77 | callback: 'startCircle', 78 | kind: 'circle', 79 | html: '⬤' 80 | } 81 | }); 82 | 83 | module.exports = { 84 | Marker: L.NewMarkerControl, 85 | Line: L.NewLineControl, 86 | Polygon: L.NewPolygonControl, 87 | Rectangle: L.NewRectangleControl, 88 | Circle: L.NewCircleControl 89 | }; 90 | -------------------------------------------------------------------------------- /test/error.test.js: -------------------------------------------------------------------------------- 1 | import tape from 'tape'; 2 | import SvgOverlay from '../src/schematic'; 3 | 4 | const leafletCss = 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.0.0-rc.2/leaflet.css'; 5 | 6 | const createMap = () => { 7 | let container = document.createElement('div'); 8 | container.style.width = container.style.height = '500px'; 9 | 10 | if (document.querySelector('#leaflet-style') === null) { 11 | let style = document.createElement('link'); 12 | style.rel = 'stylesheet'; 13 | style.type = 'text/css'; 14 | style.href = leafletCss; 15 | style.id = 'leaflet-style'; 16 | 17 | document.head.appendChild(style); 18 | } 19 | 20 | const map = L.map(container, { 21 | minZoom: 0, 22 | maxZoom: 20, 23 | center: [0, 0], 24 | zoom: 1, 25 | editable: true, 26 | crs: L.Util.extend({}, L.CRS.Simple, { 27 | infinite: false 28 | }), 29 | inertia: !L.Browser.ie 30 | }); 31 | 32 | return map; 33 | } 34 | 35 | const svgString = ` 36 | 37 | 38 | `; 39 | 40 | tape('Schematic error handling', (t) => { 41 | 42 | t.test(' Pending network status', (t) => { 43 | const map = createMap(); 44 | const schematicUrl = 'schematic_url'; 45 | let svg = new SvgOverlay(schematicUrl, { 46 | usePathContainer: true, 47 | weight: 0.25, 48 | useRaster: true, 49 | load: function(url, callback) { 50 | t.equal(url, schematicUrl, 'requested url'); 51 | // Skipp callback to emulate pending 52 | // callback(null, svgString); 53 | } 54 | }) 55 | .once('load', function () { 56 | map.fitBounds(this.getBounds(), { animate: false }); 57 | }).addTo(map); 58 | 59 | t.timeoutAfter(300); 60 | 61 | setTimeout(function() { 62 | var exception = false; 63 | 64 | try { 65 | map.removeLayer(svg); 66 | } catch (e) { 67 | exception = true; 68 | } 69 | 70 | t.notOk(exception, 'Schematic can be removed without loading'); 71 | }, 100); 72 | 73 | t.plan(2); 74 | }); 75 | 76 | t.end(); 77 | }); 78 | -------------------------------------------------------------------------------- /demo/js/svg2canvas.js: -------------------------------------------------------------------------------- 1 | var xhr = require('xhr'); 2 | var b64 = require('Base64'); 3 | var L = require('leaflet'); 4 | 5 | xhr({ 6 | uri: 'data/6.svg', 7 | headers: { 8 | "Content-Type": "image/svg+xml" 9 | } 10 | }, function (err, resp, svgString) { 11 | var d = document.querySelector('#image-map'); 12 | d.parentNode.removeChild(d); 13 | 14 | var wrapper = L.DomUtil.create('div'); 15 | wrapper.innerHTML = svgString; 16 | var svg = global.svg = wrapper.querySelector('svg'); 17 | svg.removeAttribute('width'); 18 | svg.removeAttribute('height'); 19 | svgString = wrapper.innerHTML; 20 | //svg.style.border = '1px solid green'; 21 | document.body.appendChild(svg); 22 | 23 | var winSize = L.point(document.body.clientWidth, document.body.clientHeight); 24 | var encoded = b64.btoa(unescape(encodeURIComponent(svgString))); 25 | 26 | var viewBox = svg.getAttribute('viewBox').split(' ').map(parseFloat); 27 | var tl = L.point(viewBox.slice(0,2)); 28 | var size = L.point(viewBox.slice(2, 4)); 29 | svg.style.width = size.x + 'px'; 30 | svg.style.height = size.y + 'px'; 31 | 32 | console.log(viewBox, tl, size, size.x / size.y); 33 | 34 | var img = global.img = new Image(); 35 | img.src = 'data:image/svg+xml;base64,' + encoded; 36 | img.style.border = '1px solid red'; 37 | img.style.position = 'absolute'; 38 | img.style.top = img.style.left = 0; 39 | img.style.width = size.x + 'px'; 40 | img.style.height = size.y + 'px'; 41 | L.DomUtil.setOpacity(img, 0.1); 42 | 43 | L.DomEvent.on(img, 'load', function() { 44 | var imageSize = L.point(img.offsetWidth, img.offsetHeight); 45 | console.log(imageSize, imageSize.x / imageSize.y); 46 | }); 47 | 48 | 49 | document.body.appendChild(img); 50 | 51 | var canvas = global.canvas = L.DomUtil.create('canvas', 'canvas'); 52 | //canvas.style.border = '1px solid blue'; 53 | canvas.style.position = 'absolute'; 54 | canvas.style.top = canvas.style.left = 0; 55 | 56 | canvas.width = winSize.x * 2; 57 | canvas.height = winSize.y * 2; 58 | canvas.style.width = winSize.x; 59 | canvas.style.height = winSize.y; 60 | 61 | L.DomUtil.setOpacity(canvas, 1); 62 | 63 | document.body.appendChild(canvas); 64 | 65 | canvas.getContext('2d').drawImage(img, 0, 0, size.x * 2, size.y * 2); 66 | }); -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Leaflet schematic [![npm version](https://badge.fury.io/js/leaflet-schematic.svg)](http://badge.fury.io/js/leaflet-schematic) [![CircleCI](https://circleci.com/gh/w8r/leaflet-schematic/tree/master.svg?style=shield)](https://circleci.com/gh/w8r/leaflet-schematic/tree/master) 2 | 3 | This is a set of tools to display and work with non-cartographic large 4 | high-detailed SVG schematics or blueprints. SVG is a perfect format for the 5 | task - it's vector, relatively compact, has all the means to work with templates 6 | and symbols, so it can really be a great representation and metadata container 7 | at the same time. 8 | 9 | ### Usage 10 | 11 | ```js 12 | var xhr = require('xhr'); 13 | var SVGOverlay = require('leaflet-schematic'); 14 | 15 | var map = L.map('map', { crs: L.CRS.Simple }); 16 | L.svgOverlay('/path/to/svg.svg', { 17 | load: function(url, callback) { 18 | // your/your library xhr implementation 19 | xhr({ 20 | uri: url, 21 | headers: { 22 | "Content-Type": "image/svg+xml" 23 | } 24 | }, function (err, resp, svg) { 25 | callback(err, svg); 26 | }); 27 | } 28 | }).addTo(map); 29 | ``` 30 | 31 | ### Problem 32 | 33 | The problem is that if you want to work with the SVG as with image overlay, 34 | several technical limitations and performance issues strike in: 35 | 36 | * you cannot work on larger scales with the whole canvas because of the 37 | dimension restrictions of browsers 38 | * you have to scale the drawing initially to fit the viewport on the certain 39 | zoom level 40 | * IE (as always) - I wouldn't even call that "SVG support" 41 | * `` elements have a special freaky non-compliant API which is also broken 42 | * css-transforms - unsupported 43 | * `translate() + scale()` transform on `` -_doesn't work_, use matrix 44 | * **horrible performance** - the more SVG nodes you have the slower it is 45 | 46 | ### Approach 47 | 48 | * Use leaflet viewportized layer container to render part of the `SVG` with padding 49 | * scale `SVG` to fit the viewport and zoom levels 50 | * pack `SVG` contents into moving `` 51 | * for IE - *hardcore* hacking: 52 | * render `SVG` > base64 > `` 53 | * replace `SVG` with this canvas on drag and zoom 54 | * also keep a hidden PNG rendered to overcome IE's performance drop on image 55 | scaling, somehow it works like a directive to switch the faulty smoothing off 56 | 57 | ### Know issues 58 | * SVGs without correctly provided `viewBox` work really badly and I cannot yet 59 | figure out why. I'm trying to calculate viewbox from the contents, but it 60 | still looks broken in rendered canvas 61 | 62 | ## License 63 | 64 | MIT 65 | -------------------------------------------------------------------------------- /demo/js/index.js: -------------------------------------------------------------------------------- 1 | var SvgOverlay = require('../../src/schematic'); 2 | var xhr = require('xhr'); 3 | var saveAs = require('browser-filesaver').saveAs; 4 | var Draw = require('./editable'); 5 | 6 | //global.SvgLayer = require('../../src/svglayer'); 7 | 8 | // create the slippy map 9 | var map = window.map = L.map('image-map', { 10 | minZoom: 0, 11 | maxZoom: 20, 12 | center: [0, 0], 13 | zoom: 1, 14 | editable: true, 15 | crs: L.Util.extend({}, L.CRS.Simple, { 16 | infinite: false 17 | }), 18 | inertia: !L.Browser.ie 19 | }); 20 | 21 | var controls = global.controls = [ 22 | new Draw.Line(), 23 | new Draw.Polygon(), 24 | new Draw.Rectangle() 25 | ]; 26 | controls.forEach(map.addControl, map); 27 | 28 | L.SVG.prototype.options.padding = 0.5; 29 | 30 | var svg = global.svg = null; 31 | 32 | map.on('click', function (evt) { 33 | console.log('map', evt.originalEvent.target, 34 | evt.latlng, evt, map.hasLayer(svg) ? svg.projectPoint(evt.latlng) : evt); 35 | }); 36 | 37 | var select = document.querySelector('#select-schematic'); 38 | function onSelect() { 39 | if (svg) { 40 | map.removeLayer(svg); 41 | map.off('mousemove', trackPosition, map); 42 | } 43 | 44 | svg = global.svg = new SvgOverlay(this.value, { 45 | usePathContainer: true, 46 | //opacity: 1, 47 | weight: 0.25, 48 | //useRaster: true, 49 | load: function (url, callback) { 50 | 51 | if ('pending' === url) { 52 | alert('Test network pending, no data will be shown. Switch to another svg'); 53 | return; 54 | } 55 | 56 | xhr({ 57 | uri: url, 58 | headers: { 59 | 'Content-Type': 'image/svg+xml' 60 | } 61 | }, function (err, resp, svg) { 62 | if (200 !== resp.statusCode) { 63 | err = resp.statusCode; 64 | alert('Network error', err); 65 | } 66 | callback(err, svg); 67 | }); 68 | } 69 | }) 70 | .once('load', function () { 71 | 72 | // use schematic renderer 73 | controls.forEach(function (control) { 74 | control.options.renderer = svg._renderer; 75 | }); 76 | 77 | map.fitBounds(svg.getBounds(), { animate: false }); 78 | map.on('mousemove', trackPosition, map); 79 | 80 | }).addTo(map); 81 | } 82 | 83 | L.DomEvent.on(select, 'change', onSelect); 84 | 85 | onSelect.call(select); 86 | 87 | 88 | L.DomEvent.on(document.querySelector('#dl'), 'click', function () { 89 | saveAs(new Blob([svg.exportSVG(true)]), 'schematic.svg'); 90 | }); 91 | 92 | 93 | function trackPosition(evt) { 94 | if (evt.originalEvent.shiftKey) { 95 | console.log( 96 | evt.latlng, 97 | svg.projectPoint(evt.latlng).toString(), 98 | svg.unprojectPoint(svg.projectPoint(evt.latlng)), 99 | evt.originalEvent.target 100 | ); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const L = require('leaflet'); 2 | 3 | L.Browser.phantomjs = navigator.userAgent.toLowerCase().indexOf('phantom'); 4 | 5 | // tags are broken in IE in so many ways 6 | if ('SVGElementInstance' in window) { 7 | Object.defineProperty(SVGElementInstance.prototype, 'className', { 8 | get: function () { 9 | return this.correspondingElement.className.baseVal; 10 | }, 11 | set: function (val) { 12 | this.correspondingElement.className.baseVal = val; 13 | } 14 | }); 15 | } 16 | 17 | 18 | /** 19 | * @param {*} o 20 | * @return {Boolean} 21 | */ 22 | L.DomUtil.isNode = function (o) { 23 | return ( 24 | typeof Node === 'object' ? 25 | o instanceof Node : 26 | o && typeof o === 'object' && 27 | typeof o.nodeType === 'number' && 28 | typeof o.nodeName === 'string' 29 | ); 30 | }; 31 | 32 | 33 | /** 34 | * @param {SVGElement} svg 35 | * @return {Array.} 36 | */ 37 | L.DomUtil.getSVGBBox = (svg) => { 38 | let svgBBox; 39 | const width = parseInt(svg.getAttribute('width'), 10); 40 | const height = parseInt(svg.getAttribute('height'), 10); 41 | const viewBox = svg.getAttribute('viewBox'); 42 | let bbox; 43 | 44 | if (viewBox) { 45 | bbox = viewBox.split(' ').map(parseFloat); 46 | svgBBox = [bbox[0], bbox[1], bbox[0] + bbox[2], bbox[1] + bbox[3]]; 47 | } else if (width && height) { 48 | svgBBox = [0, 0, width, height]; 49 | } else { //Calculate rendered size 50 | const clone = svg.cloneNode(true); 51 | clone.style.position = 'absolute'; 52 | clone.style.top = 0; 53 | clone.style.left = 0; 54 | clone.style.zIndex = -1; 55 | clone.style.opacity = 0; 56 | 57 | document.body.appendChild(clone); 58 | 59 | if (clone.clientWidth && clone.clientHeight) { 60 | svgBBox = [0, 0, clone.clientWidth, clone.clientHeight]; 61 | } else { 62 | svgBBox = calcSVGViewBoxFromNodes(clone); 63 | } 64 | 65 | document.body.removeChild(clone); 66 | } 67 | return svgBBox; 68 | }; 69 | 70 | 71 | /** 72 | * Simply brute force: takes all svg nodes, calculates bounding box 73 | * @param {SVGElement} svg 74 | * @return {Array.} 75 | */ 76 | function calcSVGViewBoxFromNodes(svg) { 77 | const bbox = [Infinity, Infinity, -Infinity, -Infinity]; 78 | const nodes = [].slice.call(svg.querySelectorAll('*')); 79 | const { min, max } = Math.max; 80 | 81 | for (let i = 0, len = nodes.length; i < len; i++) { 82 | let node = nodes[i]; 83 | if (node.getBBox) { 84 | node = node.getBBox(); 85 | 86 | bbox[0] = min(node.x, bbox[0]); 87 | bbox[1] = min(node.y, bbox[1]); 88 | 89 | bbox[2] = max(node.x + node.width, bbox[2]); 90 | bbox[3] = max(node.y + node.height, bbox[3]); 91 | } 92 | } 93 | return bbox; 94 | } 95 | 96 | 97 | /** 98 | * @param {String} str 99 | * @return {SVGElement} 100 | */ 101 | L.DomUtil.getSVGContainer = (str) => { 102 | const wrapper = document.createElement('div'); 103 | wrapper.innerHTML = str; 104 | return wrapper.querySelector('svg'); 105 | }; 106 | 107 | 108 | /** 109 | * @param {L.Point} translate 110 | * @param {Number} scale 111 | * @return {String} 112 | */ 113 | L.DomUtil.getMatrixString = (translate, scale) => { 114 | return 'matrix(' + 115 | [scale, 0, 0, scale, translate.x, translate.y].join(',') + ')'; 116 | }; 117 | 118 | 119 | /** 120 | * @param {SVGElement} svg 121 | * @param {SVGElement|Element} container 122 | */ 123 | L.SVG.copySVGContents = (svg, container) => { 124 | // SVG innerHTML doesn't work for SVG in IE and PhantomJS 125 | if (L.Browser.ie || L.Browser.phantomjs) { 126 | let child = svg.firstChild; 127 | do { 128 | container.appendChild(child); 129 | child = svg.firstChild; 130 | } while (child); 131 | } else { 132 | container.innerHTML = svg.innerHTML; 133 | } 134 | }; 135 | -------------------------------------------------------------------------------- /src/renderer.js: -------------------------------------------------------------------------------- 1 | const L = require('leaflet'); 2 | 3 | /** 4 | * @class L.SchematicRenderer 5 | * @param {Object} 6 | * @extends {L.SVG} 7 | */ 8 | L.SchematicRenderer = module.exports = L.SVG.extend({ 9 | 10 | options: { 11 | padding: 0.3, 12 | useRaster: L.Browser.ie || L.Browser.gecko || L.Browser.edge, 13 | interactive: true 14 | }, 15 | 16 | 17 | /** 18 | * Create additional containers for the vector features to be 19 | * transformed to live in the schematic space 20 | */ 21 | _initContainer() { 22 | L.SVG.prototype._initContainer.call(this); 23 | 24 | this._rootInvertGroup = L.SVG.create('g'); 25 | this._container.appendChild(this._rootInvertGroup); 26 | this._rootInvertGroup.appendChild(this._rootGroup); 27 | 28 | if (L.Browser.gecko) { 29 | this._container.setAttribute('pointer-events', 'visiblePainted'); 30 | } 31 | 32 | L.DomUtil.addClass(this._container, 'schematics-renderer'); 33 | }, 34 | 35 | 36 | /** 37 | * Make sure layers are not clipped 38 | * @param {L.Layer} 39 | */ 40 | _initPath(layer) { 41 | layer.options.noClip = true; 42 | L.SVG.prototype._initPath.call(this, layer); 43 | }, 44 | 45 | 46 | /** 47 | * Update call on resize, redraw, zoom change 48 | */ 49 | _update() { 50 | L.SVG.prototype._update.call(this); 51 | 52 | const schematic = this.options.schematic; 53 | const map = this._map; 54 | 55 | if (map && schematic._bounds && this._rootInvertGroup) { 56 | const topLeft = map.latLngToLayerPoint(schematic._bounds.getNorthWest()); 57 | const scale = schematic._ratio * 58 | map.options.crs.scale(map.getZoom() - schematic.options.zoomOffset); 59 | 60 | this._topLeft = topLeft; 61 | this._scale = scale; 62 | 63 | // compensate viewbox dismissal with a shift here 64 | this._rootGroup.setAttribute('transform', 65 | L.DomUtil.getMatrixString(topLeft, scale)); 66 | 67 | this._rootInvertGroup.setAttribute('transform', 68 | L.DomUtil.getMatrixString(topLeft.multiplyBy(-1 / scale), 1 / scale)); 69 | } 70 | }, 71 | 72 | 73 | /** 74 | * 1. wrap markup in another 75 | * 2. create a clipPath with the viewBox rect 76 | * 3. apply it to the around all markups 77 | * 4. remove group around schematic 78 | * 5. remove inner group around markups 79 | * 80 | * @param {Boolean=} onlyOverlays 81 | * @return {SVGElement} 82 | */ 83 | exportSVG(onlyOverlays) { 84 | const schematic = this.options.schematic; 85 | 86 | // go through every layer and make sure they're not clipped 87 | const svg = this._container.cloneNode(true); 88 | 89 | const clipPath = L.SVG.create('clipPath'); 90 | const clipRect = L.SVG.create('rect'); 91 | const clipGroup = svg.lastChild; 92 | const baseContent = svg.querySelector('.svg-overlay'); 93 | let defs = baseContent.querySelector('defs'); 94 | 95 | clipRect.setAttribute('x', schematic._bbox[0]); 96 | clipRect.setAttribute('y', schematic._bbox[1]); 97 | clipRect.setAttribute('width', schematic._bbox[2]); 98 | clipRect.setAttribute('height', schematic._bbox[3]); 99 | clipPath.appendChild(clipRect); 100 | 101 | const clipId = 'viewboxClip-' + L.Util.stamp(schematic._group); 102 | clipPath.setAttribute('id', clipId); 103 | 104 | if (!defs || onlyOverlays) { 105 | defs = L.SVG.create('defs'); 106 | svg.appendChild(defs); 107 | } 108 | defs.appendChild(clipPath); 109 | clipGroup.setAttribute('clip-path', 'url(#' + clipId + ')'); 110 | 111 | clipGroup.firstChild.setAttribute('transform', 112 | L.DomUtil.getMatrixString(this._topLeft.multiplyBy(-1 / this._scale) 113 | .add(schematic._viewBoxOffset), 1 / this._scale)); 114 | clipGroup.removeAttribute('transform'); 115 | svg.querySelector('.svg-overlay').removeAttribute('transform'); 116 | L.DomUtil.addClass(clipGroup, 'clip-group'); 117 | 118 | svg.style.transform = ''; 119 | svg.setAttribute('viewBox', schematic._bbox.join(' ')); 120 | 121 | if (onlyOverlays) { // leave only markups 122 | baseContent.parentNode.removeChild(baseContent); 123 | } 124 | 125 | const div = L.DomUtil.create('div', ''); 126 | // put container around the contents as it was 127 | div.innerHTML = (/(\]*)\>)/gi) 128 | .exec(schematic._rawData)[0] + ''; 129 | 130 | L.SVG.copySVGContents(svg, div.firstChild); 131 | 132 | return div.firstChild; 133 | } 134 | 135 | }); 136 | 137 | 138 | /** 139 | * @param {Object} 140 | * @return {L.SchematicRenderer} 141 | */ 142 | L.schematicRenderer = module.exports.schematicRenderer = 143 | (options) => new L.SchematicRenderer(options); 144 | -------------------------------------------------------------------------------- /test/schematic.test.js: -------------------------------------------------------------------------------- 1 | import tape from 'tape'; 2 | import SvgOverlay from '../src/schematic'; 3 | import b64 from 'Base64' 4 | 5 | const leafletCss = 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.0.0-rc.2/leaflet.css'; 6 | 7 | const createMap = () => { 8 | let container = document.createElement('div'); 9 | container.style.width = container.style.height = '500px'; 10 | document.body.appendChild(container); 11 | 12 | if (document.querySelector('#leaflet-style') === null) { 13 | let style = document.createElement('link'); 14 | style.rel = 'stylesheet'; 15 | style.type = 'text/css'; 16 | style.href = leafletCss; 17 | style.id = 'leaflet-style'; 18 | 19 | document.head.appendChild(style); 20 | } 21 | 22 | const map = L.map(container, { 23 | minZoom: 0, 24 | maxZoom: 20, 25 | center: [0, 0], 26 | zoom: 2, 27 | editable: true, 28 | crs: L.Util.extend({}, L.CRS.Simple, { 29 | infinite: false 30 | }), 31 | inertia: !L.Browser.ie 32 | }); 33 | 34 | return map; 35 | } 36 | 37 | const width = 500; 38 | const height = 500; 39 | 40 | const svgString = ` 41 | 42 | 43 | `; 44 | 45 | tape('Schematic layer', (t) => { 46 | 47 | t.test(' construct', (t) => { 48 | const map = createMap(); 49 | const schematicUrl = 'schematic_url'; 50 | let svg = new SvgOverlay(schematicUrl, { 51 | usePathContainer: true, 52 | //opacity: 1, 53 | weight: 0.25, 54 | load: function(url, callback) { 55 | t.equal(url, schematicUrl, 'requested url'); 56 | callback(null, svgString); 57 | } 58 | }) 59 | .once('load', function (evt) { 60 | t.equals(evt.type, 'load', 'load event'); 61 | t.equals(evt.target, this, 'evt target is schematic'); 62 | t.deepEquals(this.getBounds().toBBox(), [ 0, -500, 500, 0 ], 'bounds calculated'); 63 | t.ok(this.getDocument().querySelector('#circle'), 'content is present'); 64 | t.deepEquals(this.getOriginalSize(), L.point(500, 500), 'get original size'); 65 | map.fitBounds(this.getBounds(), { animate: false }); 66 | 67 | t.equals(evt.target.toBase64(), 68 | 'data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTAwIDUwMCI+CiAgICA8Y2lyY2xlIGN4PSIyNTAiIGN5PSIyNTAiIHI9IjEwMCIgZmlsbD0iI2ZmMDAwMCIgaWQ9ImNpcmNsZSIvPgogIDwvc3ZnPg==', 'Base64'); 69 | }).addTo(map); 70 | 71 | t.plan(7); 72 | t.end(); 73 | }); 74 | 75 | t.test(' projections', (t) => { 76 | const map = createMap(); 77 | const schematicUrl = 'schematic_url'; 78 | let svg = new SvgOverlay(schematicUrl, { 79 | usePathContainer: true, 80 | //opacity: 1, 81 | weight: 0.25, 82 | load: (url, callback) => callback(null, svgString) 83 | }) 84 | .once('load', function (evt) { 85 | map.fitBounds(this.getBounds(), { animate: false }); 86 | }) 87 | .once('add', (evt) => { 88 | t.deepEquals(evt.target.projectPoint(map.getCenter()), 89 | L.point(250, 250), 'project point'); 90 | t.deepEquals(evt.target.unprojectPoint(L.point(250, 250)), 91 | map.getCenter(), 'unproject point'); 92 | t.equals(evt.target.getRatio(), 1, 'ratio'); 93 | t.deepEquals(evt.target._transformation, 94 | new L.Transformation(1, 250, 1, 250), 'transformation'); 95 | t.deepEquals( 96 | evt.target.projectBounds(map.getBounds().pad(-0.25)).toBBox(), 97 | [ 125, 125, 375, 375 ], 'project bounds'); 98 | t.deepEquals( 99 | evt.target.unprojectBounds(L.bounds([[125, 125], [375, 375]])).toBBox(), 100 | map.getBounds().pad(-0.25).toBBox(), 'unproject bounds'); 101 | 102 | t.equals(typeof evt.target.project, 'function', 'project shortcut'); 103 | t.equals(typeof evt.target.unproject, 'function', 'unproject shortcut'); 104 | }).addTo(map); 105 | t.end(); 106 | }); 107 | 108 | t.test(' renderer', (t) => { 109 | const map = createMap(); 110 | const schematicUrl = 'schematic_url'; 111 | let svg = new SvgOverlay(schematicUrl, { 112 | usePathContainer: true, 113 | //opacity: 1, 114 | weight: 0, 115 | useRaster: true, 116 | load: (url, callback) => callback(null, svgString) 117 | }) 118 | .once('add', (evt) => { 119 | let schematic = evt.target; 120 | t.ok(schematic._renderer instanceof L.SchematicRenderer, 'schematic renderer'); 121 | t.equals(schematic._renderer.options.schematic, schematic, 'back reference is there'); 122 | t.equals(schematic.getRenderer(), schematic._renderer, 'getter for renderer'); 123 | }).addTo(map); 124 | 125 | t.end(); 126 | }); 127 | 128 | t.test(' alternative raster renderer', (t) => { 129 | t.plan(4); 130 | const map = createMap(); 131 | const schematicUrl = 'schematic_url'; 132 | let svg = new SvgOverlay(schematicUrl, { 133 | usePathContainer: true, 134 | //opacity: 1, 135 | weight: 0, 136 | useRaster: true, 137 | load: (url, callback) => callback(null, svgString) 138 | }) 139 | .once('add', (evt) => { 140 | setTimeout(() => { 141 | let schematic = evt.target; 142 | t.ok(schematic._canvasRenderer, 'canvas renderer is present'); 143 | t.ok(schematic._raster, 'raster replacement is there'); 144 | t.equals(schematic._rawData.indexOf('width="500"'), -1, 'width removed from processed'); 145 | t.equals(schematic._rawData.indexOf('height="500"'), -1, 'height removed from processed'); 146 | }); 147 | }).addTo(map); 148 | }); 149 | 150 | t.test(' export', (t) => { 151 | t.plan(10); 152 | 153 | const map = createMap(); 154 | const schematicUrl = 'schematic_url'; 155 | let svg = new SvgOverlay(schematicUrl, { 156 | usePathContainer: true, 157 | weight: 0, 158 | useRaster: true, 159 | load: (url, callback) => callback(null, svgString) 160 | }) 161 | .once('add', (evt) => { 162 | setTimeout(() => { 163 | const schematic = evt.target; 164 | const zoom = map.getZoom(); 165 | const exported = schematic.exportSVG(); 166 | 167 | //console.log(map.getZoom(), schematic.exportSVG(true), matrix, 1 / Math.pow(2, map.getZoom())); 168 | 169 | t.equals(Object.prototype.toString.call(exported), '[object SVGSVGElement]', 'exports SVG element by default'); 170 | t.equals(exported.firstChild.className.baseVal, 'svg-overlay', 'initial document is included'); 171 | t.ok(exported.firstChild.querySelector('circle'), 'contents are there'); 172 | t.equals(typeof schematic.exportSVG(true), 'string', 'optionally exports string'); 173 | 174 | let exportedOverlay = schematic.exportSVG(false, true); 175 | t.equals(exportedOverlay.firstChild.className.baseVal, 'clip-group', 'optionally exports only clipped overlays as node'); 176 | t.equals(typeof schematic.exportSVG(true, true), 'string', 'or as a string'); 177 | 178 | let matrix = exported.querySelector('.clip-group').firstChild.getAttribute('transform'); 179 | matrix = matrix.substring(7, matrix.length - 1).split(',').map(parseFloat); 180 | 181 | let scale = 1 / Math.pow(2, map.getZoom()); 182 | t.equals(matrix[0], scale, 'x-scale'); 183 | t.equals(matrix[3], scale, 'y-scale'); 184 | 185 | t.equals(matrix[4], -width / (2 * Math.pow(2, zoom)), 'x positioned at scale'); 186 | t.equals(matrix[4], -height / (2 * Math.pow(2, zoom)), 'y positioned at scale'); 187 | }); 188 | }).addTo(map); 189 | }); 190 | 191 | t.end(); 192 | }); 193 | -------------------------------------------------------------------------------- /dist/L.Schematic.min.js: -------------------------------------------------------------------------------- 1 | (function(t){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=t()}else if(typeof define==="function"&&define.amd){define([],t)}else{var e;if(typeof window!=="undefined"){e=window}else if(typeof global!=="undefined"){e=global}else if(typeof self!=="undefined"){e=self}else{e=this}(e.L||(e.L={})).Schematic=t()}})(function(){var o,t,e;return function(){function u(o,s,a){function h(i,t){if(!s[i]){if(!o[i]){var e="function"==typeof require&&require;if(!t&&e)return e(i,!0);if(l)return l(i,!0);var n=new Error("Cannot find module '"+i+"'");throw n.code="MODULE_NOT_FOUND",n}var r=s[i]={exports:{}};o[i][0].call(r.exports,function(t){var e=o[i][1][t];return h(e||t)},r,r.exports,u,o,s,a)}return s[i].exports}for(var l="function"==typeof require&&require,t=0;t>8-r%1*8)){n=e.charCodeAt(r+=3/4);if(n>255){throw new h("'btoa' failed: The string to be encoded contains characters outside of the Latin1 range.")}i=i<<8|n}return s}function e(t){var e=String(t).replace(/[=]+$/,"");if(e.length%4===1){throw new h("'atob' failed: The string to be decoded is not correctly encoded.")}for(var i=0,n,r,o=0,s="";r=e.charAt(o++);~r&&(n=i%4?n*64+r:r,i++%4)?s+=String.fromCharCode(255&n>>(-2*i&6)):0){r=a.indexOf(r)}return s}return{btoa:t,atob:e}})},{}],3:[function(t,e,i){(function(t){"use strict";var o=typeof window!=="undefined"?window["L"]:typeof t!=="undefined"?t["L"]:null;o.Bounds.prototype.toBBox=function(){return[this.min.x,this.min.y,this.max.x,this.max.y]};o.Bounds.prototype.scale=function(t){var e=this.max,i=this.min;var n=(e.x-i.x)/2*(t-1);var r=(e.y-i.y)/2*(t-1);return new o.Bounds([[i.x-n,i.y-r],[e.x+n,e.y+r]])};o.LatLngBounds.prototype.toBBox=function(){return[this.getWest(),this.getSouth(),this.getEast(),this.getNorth()]};o.LatLngBounds.prototype.scale=function(t){var e=this._northEast;var i=this._southWest;var n=(e.lng-i.lng)/2*(t-1);var r=(e.lat-i.lat)/2*(t-1);return new o.LatLngBounds([[i.lat-r,i.lng-n],[e.lat+r,e.lng+n]])}}).call(this,typeof global!=="undefined"?global:typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{}],4:[function(t,e,i){(function(t){"use strict";var d=typeof window!=="undefined"?window["L"]:typeof t!=="undefined"?t["L"]:null;d.SchematicRenderer=e.exports=d.SVG.extend({options:{padding:.3,useRaster:d.Browser.ie||d.Browser.gecko||d.Browser.edge,interactive:true},_initContainer:function t(){d.SVG.prototype._initContainer.call(this);this._rootInvertGroup=d.SVG.create("g");this._container.appendChild(this._rootInvertGroup);this._rootInvertGroup.appendChild(this._rootGroup);if(d.Browser.gecko){this._container.setAttribute("pointer-events","visiblePainted")}d.DomUtil.addClass(this._container,"schematics-renderer")},_initPath:function t(e){e.options.noClip=true;d.SVG.prototype._initPath.call(this,e)},_update:function t(){d.SVG.prototype._update.call(this);var e=this.options.schematic;var i=this._map;if(i&&e._bounds&&this._rootInvertGroup){var n=i.latLngToLayerPoint(e._bounds.getNorthWest());var r=e._ratio*i.options.crs.scale(i.getZoom()-e.options.zoomOffset);this._topLeft=n;this._scale=r;this._rootGroup.setAttribute("transform",d.DomUtil.getMatrixString(n,r));this._rootInvertGroup.setAttribute("transform",d.DomUtil.getMatrixString(n.multiplyBy(-1/r),1/r))}},exportSVG:function t(e){var i=this.options.schematic;var n=this._container.cloneNode(true);var r=d.SVG.create("clipPath");var o=d.SVG.create("rect");var s=n.lastChild;var a=n.querySelector(".svg-overlay");var h=a.querySelector("defs");o.setAttribute("x",i._bbox[0]);o.setAttribute("y",i._bbox[1]);o.setAttribute("width",i._bbox[2]);o.setAttribute("height",i._bbox[3]);r.appendChild(o);var l="viewboxClip-"+d.Util.stamp(i._group);r.setAttribute("id",l);if(!h||e){h=d.SVG.create("defs");n.appendChild(h)}h.appendChild(r);s.setAttribute("clip-path","url(#"+l+")");s.firstChild.setAttribute("transform",d.DomUtil.getMatrixString(this._topLeft.multiplyBy(-1/this._scale).add(i._viewBoxOffset),1/this._scale));s.removeAttribute("transform");n.querySelector(".svg-overlay").removeAttribute("transform");d.DomUtil.addClass(s,"clip-group");n.style.transform="";n.setAttribute("viewBox",i._bbox.join(" "));if(e){a.parentNode.removeChild(a)}var u=d.DomUtil.create("div","");u.innerHTML=/(\]*)\>)/gi.exec(i._rawData)[0]+"";d.SVG.copySVGContents(n,u.firstChild);return u.firstChild}});d.schematicRenderer=e.exports.schematicRenderer=function(t){return new d.SchematicRenderer(t)}}).call(this,typeof global!=="undefined"?global:typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{}],5:[function(e,n,t){(function(t){"use strict";var h=typeof window!=="undefined"?window["L"]:typeof t!=="undefined"?t["L"]:null;var i=e("Base64");var r=e("./renderer");e("./bounds");e("./utils"); 2 | /** 3 | * Schematic layer to work with SVG schematics or blueprints in Leaflet 4 | * 5 | * @author Alexander Milevski 6 | * @license MIT 7 | * @preserve 8 | * @class Schematic 9 | * @extends {L.Rectangle} 10 | */h.Schematic=n.exports=h.Rectangle.extend({options:{opacity:0,fillOpacity:0,weight:1,adjustToScreen:true,zoomOffset:0,interactive:false,useRaster:h.Browser.ie||h.Browser.gecko||h.Browser.edge},initialize:function t(e,i,n){this._svg=e;this._initialWidth="";this._initialHeight="";if(!(i instanceof h.LatLngBounds)){n=i;i=null}n.renderer=new r({schematic:this});this._bounds=i;this._ratio=1;this._size=null;this._origin=null;this._transformation=null;this._base64encoded="";this._rawData="";this._viewBoxOffset=h.point(0,0);this._ready=false;if(typeof e==="string"&&!/\ 13 | * @license MIT 14 | * @preserve 15 | * @class Schematic 16 | * @extends {L.Rectangle} 17 | */ 18 | L.Schematic = module.exports = L.Rectangle.extend({ 19 | 20 | options: { 21 | opacity: 0, 22 | fillOpacity: 0, 23 | weight: 1, 24 | adjustToScreen: true, 25 | 26 | // hardcode zoom offset to snap to some level 27 | zoomOffset: 0, 28 | interactive: false, 29 | useRaster: L.Browser.ie || L.Browser.gecko || L.Browser.edge 30 | }, 31 | 32 | 33 | /** 34 | * @constructor 35 | * @param {String} svg SVG string or URL 36 | * @param {L.LatLngBounds} bounds 37 | * @param {Object=} options 38 | */ 39 | initialize(svg, bounds, options) { 40 | 41 | /** 42 | * @type {String} 43 | */ 44 | this._svg = svg; 45 | 46 | /** 47 | * Initial svg width, cause we will have to get rid of that to maintain 48 | * the aspect ratio 49 | * 50 | * @type {String} 51 | */ 52 | this._initialWidth = ''; 53 | 54 | 55 | /** 56 | * Initial svg height 57 | * @type {String} 58 | */ 59 | this._initialHeight = ''; 60 | 61 | if (!(bounds instanceof L.LatLngBounds)) { 62 | options = bounds; 63 | bounds = null; 64 | } 65 | 66 | options.renderer = new Renderer({ 67 | schematic: this 68 | // padding: options.padding || this.options.padding || 0.25 69 | }); 70 | 71 | /** 72 | * @type {L.LatLngBounds} 73 | */ 74 | this._bounds = bounds; 75 | 76 | /** 77 | * @type {Number} 78 | */ 79 | this._ratio = 1; 80 | 81 | 82 | /** 83 | * @type {L.Point} 84 | */ 85 | this._size = null; 86 | 87 | 88 | /** 89 | * @type {L.Point} 90 | */ 91 | this._origin = null; 92 | 93 | 94 | /** 95 | * @type {L.Transformation} 96 | */ 97 | this._transformation = null; 98 | 99 | 100 | /** 101 | * @type {String} 102 | */ 103 | this._base64encoded = ''; 104 | 105 | 106 | /** 107 | * @type {String} 108 | */ 109 | this._rawData = ''; 110 | 111 | 112 | /** 113 | * @type {L.Point} 114 | */ 115 | this._viewBoxOffset = L.point(0, 0); 116 | 117 | 118 | /** 119 | * @type {Boolean} 120 | */ 121 | this._ready = false; 122 | 123 | 124 | if (typeof svg === 'string' && !/\ { 532 | L.point(img.offsetWidth, img.offsetHeight); 533 | this._reset(); 534 | }); 535 | img.style.opacity = 0; 536 | img.style.zIndex = -9999; 537 | img.style.pointerEvents = 'none'; 538 | 539 | if (this._raster) { 540 | this._raster.parentNode.removeChild(this._raster); 541 | this._raster = null; 542 | } 543 | 544 | L.DomUtil.addClass(img, 'schematic-image'); 545 | this._renderer._container.parentNode 546 | .insertBefore(img, this._renderer._container); 547 | this._raster = img; 548 | return this; 549 | }, 550 | 551 | 552 | /** 553 | * Convert SVG data to base64 for rasterization 554 | * @return {String} base64 encoded SVG 555 | */ 556 | toBase64() { 557 | // console.time('base64'); 558 | const base64 = this._base64encoded || 559 | b64.btoa(unescape(encodeURIComponent(this._processedData))); 560 | this._base64encoded = base64; 561 | // console.timeEnd('base64'); 562 | 563 | return 'data:image/svg+xml;base64,' + base64; 564 | }, 565 | 566 | 567 | /** 568 | * Redraw canvas on real changes: zoom, viewreset 569 | * @param {L.Point} topLeft 570 | * @param {Number} scale 571 | */ 572 | _redrawCanvas(topLeft, scale) { 573 | if (!this._raster) { 574 | return; 575 | } 576 | 577 | const size = this.getOriginalSize().multiplyBy(scale); 578 | const ctx = this._canvasRenderer._ctx; 579 | 580 | L.Util.requestAnimFrame(function () { 581 | ctx.drawImage(this._raster, topLeft.x, topLeft.y, size.x, size.y); 582 | }, this); 583 | }, 584 | 585 | 586 | /** 587 | * Toggle canvas instead of SVG when dragging 588 | */ 589 | _showRaster() { 590 | if (this._canvasRenderer && !this._rasterShown) { 591 | // console.time('show'); 592 | // `display` rule somehow appears to be faster in IE, FF 593 | // this._canvasRenderer._container.style.visibility = 'visible'; 594 | this._canvasRenderer._container.style.display = 'block'; 595 | this._group.style.display = 'none'; 596 | this._rasterShown = true; 597 | // console.timeEnd('show'); 598 | } 599 | }, 600 | 601 | 602 | /** 603 | * Swap back to SVG 604 | */ 605 | _hideRaster() { 606 | if (this._canvasRenderer && this._rasterShown) { 607 | // console.time('hide'); 608 | // `display` rule somehow appears to be faster in IE, FF 609 | // this._canvasRenderer._container.style.visibility = 'hidden'; 610 | this._canvasRenderer._container.style.display = 'none'; 611 | this._group.style.display = 'block'; 612 | this._rasterShown = false; 613 | // console.timeEnd('hide'); 614 | } 615 | }, 616 | 617 | 618 | /** 619 | * IE-only 620 | * Replace SVG with canvas before drag 621 | */ 622 | _onPreDrag() { 623 | if (this.options.useRaster) { 624 | this._showRaster(); 625 | } 626 | }, 627 | 628 | 629 | /** 630 | * Drag end: put SVG back in IE 631 | */ 632 | _onDragEnd() { 633 | if (this.options.useRaster) { 634 | this._hideRaster(); 635 | } 636 | } 637 | 638 | }); 639 | 640 | 641 | // aliases 642 | L.Schematic.prototype.project = L.Schematic.prototype.projectPoint; 643 | L.Schematic.prototype.unproject = L.Schematic.prototype.unprojectPoint; 644 | 645 | 646 | /** 647 | * Factory 648 | * @param {String} svg SVG string or URL 649 | * @param {L.LatLngBounds} bounds 650 | * @param {Object=} options 651 | * @return {L.Schematic} 652 | */ 653 | L.schematic = function (svg, bounds, options) { 654 | return new L.Schematic(svg, bounds, options); 655 | }; 656 | -------------------------------------------------------------------------------- /demo/data/export.svg: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 15 | image/svg+xml 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | Non-inverting 59 | input 60 | 61 | 3 62 | 63 | 64 | 65 | 66 | 67 | 68 | 1 kΩ 69 | 70 | 71 | 72 | 1 73 | 74 | Offset 75 | null 76 | 77 | 78 | 79 | 80 | 81 | 82 | 50 kΩ 83 | 84 | 85 | 86 | 87 | 88 | 1 kΩ 89 | 90 | 91 | 92 | 5 93 | 94 | Offset 95 | null 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | Inverting 105 | input 106 | 107 | 2 108 | 109 | 110 | 111 | 7 112 | VS+ 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 5 kΩ 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 39 kΩ 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 50 kΩ 148 | 149 | 150 | 151 | 152 | 153 | 50 Ω 154 | 155 | 156 | 157 | 158 | 7.5 kΩ 159 | 160 | 161 | 4.5 kΩ 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 30 pF 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 25 Ω 185 | 186 | 187 | 188 | 50 Ω 189 | 190 | 191 | 192 | 193 | 194 | 195 | 6 196 | Output 197 | 198 | 4 199 | VS 200 | 201 | 202 | Q1 203 | Q8 204 | Q9 205 | Q12 206 | Q13 207 | Q14 208 | Q17 209 | Q20 210 | Q2 211 | Q3 212 | Q4 213 | Q7 214 | Q5 215 | Q6 216 | Q10 217 | Q11 218 | Q22 219 | Q15 220 | Q19 221 | Q16 222 | 223 | -------------------------------------------------------------------------------- /demo/data/export2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | image/svg+xml 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | Non-inverting 46 | input 47 | 48 | 3 49 | 50 | 51 | 52 | 53 | 54 | 55 | 1 kΩ 56 | 57 | 58 | 59 | 1 60 | 61 | Offset 62 | null 63 | 64 | 65 | 66 | 67 | 68 | 69 | 50 kΩ 70 | 71 | 72 | 73 | 74 | 75 | 1 kΩ 76 | 77 | 78 | 79 | 5 80 | 81 | Offset 82 | null 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | Inverting 92 | input 93 | 94 | 2 95 | 96 | 97 | 98 | 7 99 | VS+ 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 5 kΩ 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 39 kΩ 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 50 kΩ 135 | 136 | 137 | 138 | 139 | 140 | 50 Ω 141 | 142 | 143 | 144 | 145 | 7.5 kΩ 146 | 147 | 148 | 4.5 kΩ 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 30 pF 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 25 Ω 172 | 173 | 174 | 175 | 50 Ω 176 | 177 | 178 | 179 | 180 | 181 | 182 | 6 183 | Output 184 | 185 | 4 186 | VS 187 | 188 | 189 | Q1 190 | Q8 191 | Q9 192 | Q12 193 | Q13 194 | Q14 195 | Q17 196 | Q20 197 | Q2 198 | Q3 199 | Q4 200 | Q7 201 | Q5 202 | Q6 203 | Q10 204 | Q11 205 | Q22 206 | Q15 207 | Q19 208 | Q16 209 | 210 | -------------------------------------------------------------------------------- /dist/L.Schematic.js: -------------------------------------------------------------------------------- 1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}(g.L || (g.L = {})).Schematic = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i> 8 - idx % 1 * 8) 49 | ) { 50 | charCode = str.charCodeAt (idx += 3 / 4); 51 | if (charCode > 0xFF) { 52 | throw new InvalidCharacterError ("'btoa' failed: The string to be encoded contains characters outside of the Latin1 range."); 53 | } 54 | block = block << 8 | charCode; 55 | } 56 | return output; 57 | } 58 | 59 | // decoder 60 | // [https://gist.github.com/1020396] by [https://github.com/atk] 61 | function atob(input) { 62 | var str = (String (input)).replace (/[=]+$/, ''); // #31: ExtendScript bad parse of /= 63 | if (str.length % 4 === 1) { 64 | throw new InvalidCharacterError ("'atob' failed: The string to be decoded is not correctly encoded."); 65 | } 66 | for ( 67 | // initialize result and counters 68 | var bc = 0, bs, buffer, idx = 0, output = ''; 69 | // get next character 70 | buffer = str.charAt (idx++); // eslint-disable-line no-cond-assign 71 | // character found in table? initialize bit storage and add its ascii value; 72 | ~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer, 73 | // and if not first of each 4 characters, 74 | // convert the first 8 bits to one ascii character 75 | bc++ % 4) ? output += String.fromCharCode (255 & bs >> (-2 * bc & 6)) : 0 76 | ) { 77 | // try to find character in table (0-63, not found => -1) 78 | buffer = chars.indexOf (buffer); 79 | } 80 | return output; 81 | } 82 | 83 | return {btoa: btoa, atob: atob}; 84 | 85 | })); 86 | 87 | },{}],3:[function(require,module,exports){ 88 | (function (global){ 89 | "use strict"; 90 | 91 | var L = typeof window !== "undefined" ? window['L'] : typeof global !== "undefined" ? global['L'] : null; 92 | 93 | /** 94 | * @return {Array.} 95 | */ 96 | L.Bounds.prototype.toBBox = function () { 97 | return [this.min.x, this.min.y, this.max.x, this.max.y]; 98 | }; 99 | 100 | /** 101 | * @param {Number} value 102 | * @return {L.Bounds} 103 | */ 104 | L.Bounds.prototype.scale = function (value) { 105 | var max = this.max, 106 | min = this.min; 107 | 108 | var deltaX = (max.x - min.x) / 2 * (value - 1); 109 | var deltaY = (max.y - min.y) / 2 * (value - 1); 110 | 111 | return new L.Bounds([[min.x - deltaX, min.y - deltaY], [max.x + deltaX, max.y + deltaY]]); 112 | }; 113 | 114 | /** 115 | * @return {Array.} 116 | */ 117 | L.LatLngBounds.prototype.toBBox = function () { 118 | return [this.getWest(), this.getSouth(), this.getEast(), this.getNorth()]; 119 | }; 120 | 121 | /** 122 | * @param {Number} value 123 | * @return {L.LatLngBounds} 124 | */ 125 | L.LatLngBounds.prototype.scale = function (value) { 126 | var ne = this._northEast; 127 | var sw = this._southWest; 128 | var deltaX = (ne.lng - sw.lng) / 2 * (value - 1); 129 | var deltaY = (ne.lat - sw.lat) / 2 * (value - 1); 130 | 131 | return new L.LatLngBounds([[sw.lat - deltaY, sw.lng - deltaX], [ne.lat + deltaY, ne.lng + deltaX]]); 132 | }; 133 | 134 | }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) 135 | },{}],4:[function(require,module,exports){ 136 | (function (global){ 137 | "use strict"; 138 | 139 | var L = typeof window !== "undefined" ? window['L'] : typeof global !== "undefined" ? global['L'] : null; 140 | 141 | /** 142 | * @class L.SchematicRenderer 143 | * @param {Object} 144 | * @extends {L.SVG} 145 | */ 146 | L.SchematicRenderer = module.exports = L.SVG.extend({ 147 | 148 | options: { 149 | padding: 0.3, 150 | useRaster: L.Browser.ie || L.Browser.gecko || L.Browser.edge, 151 | interactive: true 152 | }, 153 | 154 | /** 155 | * Create additional containers for the vector features to be 156 | * transformed to live in the schematic space 157 | */ 158 | _initContainer: function _initContainer() { 159 | L.SVG.prototype._initContainer.call(this); 160 | 161 | this._rootInvertGroup = L.SVG.create('g'); 162 | this._container.appendChild(this._rootInvertGroup); 163 | this._rootInvertGroup.appendChild(this._rootGroup); 164 | 165 | if (L.Browser.gecko) { 166 | this._container.setAttribute('pointer-events', 'visiblePainted'); 167 | } 168 | 169 | L.DomUtil.addClass(this._container, 'schematics-renderer'); 170 | }, 171 | 172 | 173 | /** 174 | * Make sure layers are not clipped 175 | * @param {L.Layer} 176 | */ 177 | _initPath: function _initPath(layer) { 178 | layer.options.noClip = true; 179 | L.SVG.prototype._initPath.call(this, layer); 180 | }, 181 | 182 | 183 | /** 184 | * Update call on resize, redraw, zoom change 185 | */ 186 | _update: function _update() { 187 | L.SVG.prototype._update.call(this); 188 | 189 | var schematic = this.options.schematic; 190 | var map = this._map; 191 | 192 | if (map && schematic._bounds && this._rootInvertGroup) { 193 | var topLeft = map.latLngToLayerPoint(schematic._bounds.getNorthWest()); 194 | var scale = schematic._ratio * map.options.crs.scale(map.getZoom() - schematic.options.zoomOffset); 195 | 196 | this._topLeft = topLeft; 197 | this._scale = scale; 198 | 199 | // compensate viewbox dismissal with a shift here 200 | this._rootGroup.setAttribute('transform', L.DomUtil.getMatrixString(topLeft, scale)); 201 | 202 | this._rootInvertGroup.setAttribute('transform', L.DomUtil.getMatrixString(topLeft.multiplyBy(-1 / scale), 1 / scale)); 203 | } 204 | }, 205 | 206 | 207 | /** 208 | * 1. wrap markup in another 209 | * 2. create a clipPath with the viewBox rect 210 | * 3. apply it to the around all markups 211 | * 4. remove group around schematic 212 | * 5. remove inner group around markups 213 | * 214 | * @param {Boolean=} onlyOverlays 215 | * @return {SVGElement} 216 | */ 217 | exportSVG: function exportSVG(onlyOverlays) { 218 | var schematic = this.options.schematic; 219 | 220 | // go through every layer and make sure they're not clipped 221 | var svg = this._container.cloneNode(true); 222 | 223 | var clipPath = L.SVG.create('clipPath'); 224 | var clipRect = L.SVG.create('rect'); 225 | var clipGroup = svg.lastChild; 226 | var baseContent = svg.querySelector('.svg-overlay'); 227 | var defs = baseContent.querySelector('defs'); 228 | 229 | clipRect.setAttribute('x', schematic._bbox[0]); 230 | clipRect.setAttribute('y', schematic._bbox[1]); 231 | clipRect.setAttribute('width', schematic._bbox[2]); 232 | clipRect.setAttribute('height', schematic._bbox[3]); 233 | clipPath.appendChild(clipRect); 234 | 235 | var clipId = 'viewboxClip-' + L.Util.stamp(schematic._group); 236 | clipPath.setAttribute('id', clipId); 237 | 238 | if (!defs || onlyOverlays) { 239 | defs = L.SVG.create('defs'); 240 | svg.appendChild(defs); 241 | } 242 | defs.appendChild(clipPath); 243 | clipGroup.setAttribute('clip-path', 'url(#' + clipId + ')'); 244 | 245 | clipGroup.firstChild.setAttribute('transform', L.DomUtil.getMatrixString(this._topLeft.multiplyBy(-1 / this._scale).add(schematic._viewBoxOffset), 1 / this._scale)); 246 | clipGroup.removeAttribute('transform'); 247 | svg.querySelector('.svg-overlay').removeAttribute('transform'); 248 | L.DomUtil.addClass(clipGroup, 'clip-group'); 249 | 250 | svg.style.transform = ''; 251 | svg.setAttribute('viewBox', schematic._bbox.join(' ')); 252 | 253 | if (onlyOverlays) { 254 | // leave only markups 255 | baseContent.parentNode.removeChild(baseContent); 256 | } 257 | 258 | var div = L.DomUtil.create('div', ''); 259 | // put container around the contents as it was 260 | div.innerHTML = /(\]*)\>)/gi.exec(schematic._rawData)[0] + ''; 261 | 262 | L.SVG.copySVGContents(svg, div.firstChild); 263 | 264 | return div.firstChild; 265 | } 266 | }); 267 | 268 | /** 269 | * @param {Object} 270 | * @return {L.SchematicRenderer} 271 | */ 272 | L.schematicRenderer = module.exports.schematicRenderer = function (options) { 273 | return new L.SchematicRenderer(options); 274 | }; 275 | 276 | }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) 277 | },{}],5:[function(require,module,exports){ 278 | (function (global){ 279 | "use strict"; 280 | 281 | var L = typeof window !== "undefined" ? window['L'] : typeof global !== "undefined" ? global['L'] : null; 282 | var b64 = require('Base64'); 283 | var Renderer = require('./renderer'); 284 | 285 | require('./bounds'); 286 | require('./utils'); 287 | 288 | /** 289 | * Schematic layer to work with SVG schematics or blueprints in Leaflet 290 | * 291 | * @author Alexander Milevski 292 | * @license MIT 293 | * @preserve 294 | * @class Schematic 295 | * @extends {L.Rectangle} 296 | */ 297 | L.Schematic = module.exports = L.Rectangle.extend({ 298 | 299 | options: { 300 | opacity: 0, 301 | fillOpacity: 0, 302 | weight: 1, 303 | adjustToScreen: true, 304 | 305 | // hardcode zoom offset to snap to some level 306 | zoomOffset: 0, 307 | interactive: false, 308 | useRaster: L.Browser.ie || L.Browser.gecko || L.Browser.edge 309 | }, 310 | 311 | /** 312 | * @constructor 313 | * @param {String} svg SVG string or URL 314 | * @param {L.LatLngBounds} bounds 315 | * @param {Object=} options 316 | */ 317 | initialize: function initialize(svg, bounds, options) { 318 | 319 | /** 320 | * @type {String} 321 | */ 322 | this._svg = svg; 323 | 324 | /** 325 | * Initial svg width, cause we will have to get rid of that to maintain 326 | * the aspect ratio 327 | * 328 | * @type {String} 329 | */ 330 | this._initialWidth = ''; 331 | 332 | /** 333 | * Initial svg height 334 | * @type {String} 335 | */ 336 | this._initialHeight = ''; 337 | 338 | if (!(bounds instanceof L.LatLngBounds)) { 339 | options = bounds; 340 | bounds = null; 341 | } 342 | 343 | options.renderer = new Renderer({ 344 | schematic: this 345 | // padding: options.padding || this.options.padding || 0.25 346 | }); 347 | 348 | /** 349 | * @type {L.LatLngBounds} 350 | */ 351 | this._bounds = bounds; 352 | 353 | /** 354 | * @type {Number} 355 | */ 356 | this._ratio = 1; 357 | 358 | /** 359 | * @type {L.Point} 360 | */ 361 | this._size = null; 362 | 363 | /** 364 | * @type {L.Point} 365 | */ 366 | this._origin = null; 367 | 368 | /** 369 | * @type {L.Transformation} 370 | */ 371 | this._transformation = null; 372 | 373 | /** 374 | * @type {String} 375 | */ 376 | this._base64encoded = ''; 377 | 378 | /** 379 | * @type {String} 380 | */ 381 | this._rawData = ''; 382 | 383 | /** 384 | * @type {L.Point} 385 | */ 386 | this._viewBoxOffset = L.point(0, 0); 387 | 388 | /** 389 | * @type {Boolean} 390 | */ 391 | this._ready = false; 392 | 393 | if (typeof svg === 'string' && !/\ tags are broken in IE in so many ways 905 | if ('SVGElementInstance' in window) { 906 | Object.defineProperty(SVGElementInstance.prototype, 'className', { 907 | get: function get() { 908 | return this.correspondingElement.className.baseVal; 909 | }, 910 | set: function set(val) { 911 | this.correspondingElement.className.baseVal = val; 912 | } 913 | }); 914 | } 915 | 916 | /** 917 | * @param {*} o 918 | * @return {Boolean} 919 | */ 920 | L.DomUtil.isNode = function (o) { 921 | return (typeof Node === "undefined" ? "undefined" : _typeof(Node)) === 'object' ? o instanceof Node : o && (typeof o === "undefined" ? "undefined" : _typeof(o)) === 'object' && typeof o.nodeType === 'number' && typeof o.nodeName === 'string'; 922 | }; 923 | 924 | /** 925 | * @param {SVGElement} svg 926 | * @return {Array.} 927 | */ 928 | L.DomUtil.getSVGBBox = function (svg) { 929 | var svgBBox = void 0; 930 | var width = parseInt(svg.getAttribute('width'), 10); 931 | var height = parseInt(svg.getAttribute('height'), 10); 932 | var viewBox = svg.getAttribute('viewBox'); 933 | var bbox = void 0; 934 | 935 | if (viewBox) { 936 | bbox = viewBox.split(' ').map(parseFloat); 937 | svgBBox = [bbox[0], bbox[1], bbox[0] + bbox[2], bbox[1] + bbox[3]]; 938 | } else if (width && height) { 939 | svgBBox = [0, 0, width, height]; 940 | } else { 941 | //Calculate rendered size 942 | var clone = svg.cloneNode(true); 943 | clone.style.position = 'absolute'; 944 | clone.style.top = 0; 945 | clone.style.left = 0; 946 | clone.style.zIndex = -1; 947 | clone.style.opacity = 0; 948 | 949 | document.body.appendChild(clone); 950 | 951 | if (clone.clientWidth && clone.clientHeight) { 952 | svgBBox = [0, 0, clone.clientWidth, clone.clientHeight]; 953 | } else { 954 | svgBBox = calcSVGViewBoxFromNodes(clone); 955 | } 956 | 957 | document.body.removeChild(clone); 958 | } 959 | return svgBBox; 960 | }; 961 | 962 | /** 963 | * Simply brute force: takes all svg nodes, calculates bounding box 964 | * @param {SVGElement} svg 965 | * @return {Array.} 966 | */ 967 | function calcSVGViewBoxFromNodes(svg) { 968 | var bbox = [Infinity, Infinity, -Infinity, -Infinity]; 969 | var nodes = [].slice.call(svg.querySelectorAll('*')); 970 | var _Math$max = Math.max, 971 | min = _Math$max.min, 972 | max = _Math$max.max; 973 | 974 | 975 | for (var i = 0, len = nodes.length; i < len; i++) { 976 | var node = nodes[i]; 977 | if (node.getBBox) { 978 | node = node.getBBox(); 979 | 980 | bbox[0] = min(node.x, bbox[0]); 981 | bbox[1] = min(node.y, bbox[1]); 982 | 983 | bbox[2] = max(node.x + node.width, bbox[2]); 984 | bbox[3] = max(node.y + node.height, bbox[3]); 985 | } 986 | } 987 | return bbox; 988 | } 989 | 990 | /** 991 | * @param {String} str 992 | * @return {SVGElement} 993 | */ 994 | L.DomUtil.getSVGContainer = function (str) { 995 | var wrapper = document.createElement('div'); 996 | wrapper.innerHTML = str; 997 | return wrapper.querySelector('svg'); 998 | }; 999 | 1000 | /** 1001 | * @param {L.Point} translate 1002 | * @param {Number} scale 1003 | * @return {String} 1004 | */ 1005 | L.DomUtil.getMatrixString = function (translate, scale) { 1006 | return 'matrix(' + [scale, 0, 0, scale, translate.x, translate.y].join(',') + ')'; 1007 | }; 1008 | 1009 | /** 1010 | * @param {SVGElement} svg 1011 | * @param {SVGElement|Element} container 1012 | */ 1013 | L.SVG.copySVGContents = function (svg, container) { 1014 | // SVG innerHTML doesn't work for SVG in IE and PhantomJS 1015 | if (L.Browser.ie || L.Browser.phantomjs) { 1016 | var child = svg.firstChild; 1017 | do { 1018 | container.appendChild(child); 1019 | child = svg.firstChild; 1020 | } while (child); 1021 | } else { 1022 | container.innerHTML = svg.innerHTML; 1023 | } 1024 | }; 1025 | 1026 | }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) 1027 | },{}]},{},[1])(1) 1028 | }); 1029 | -------------------------------------------------------------------------------- /demo/data/2.svg: -------------------------------------------------------------------------------- 1 | 2 | 20 | 22 | 23 | 25 | image/svg+xml 26 | 28 | 29 | 30 | 31 | 32 | 55 | 57 | 64 | 74 | 75 | 77 | 88 | 89 | 92 | 96 | 97 | 100 | 111 | 118 | 124 | 130 | 135 | 136 | 139 | 150 | 157 | 163 | 169 | 174 | 175 | 178 | 184 | 190 | 191 | 192 | 201 | 208 | 217 | 220 | Non-inverting 225 | input 230 | 231 | 3 236 | 243 | 252 | 259 | 268 | 275 | 284 | 1 kΩ 289 | 294 | 301 | 310 | 1 315 | 318 | Offset 323 | null 328 | 329 | 338 | 345 | 352 | 359 | 368 | 50 kΩ 373 | 380 | 389 | 396 | 403 | 412 | 1 kΩ 417 | 424 | 431 | 440 | 5 445 | 448 | Offset 453 | null 458 | 459 | 468 | 475 | 482 | 491 | 498 | 507 | 510 | Inverting 515 | input 520 | 521 | 2 526 | 533 | 538 | 547 | 7 552 | VS+ 565 | 566 | 567 | 576 | 583 | 588 | 595 | 602 | 611 | 618 | 625 | 632 | 637 | 646 | 653 | 662 | 5 kΩ 667 | 674 | 683 | 690 | 697 | 702 | 711 | 718 | 725 | 734 | 741 | 39 kΩ 746 | 755 | 760 | 767 | 776 | 783 | 790 | 799 | 50 kΩ 804 | 811 | 820 | 827 | 834 | 843 | 50 Ω 848 | 855 | 862 | 869 | 878 | 7.5 kΩ 883 | 890 | 899 | 4.5 kΩ 904 | 911 | 920 | 927 | 934 | 941 | 948 | 957 | 962 | 30 pF 967 | 976 | 983 | 988 | 995 | 1002 | 1011 | 1020 | 1027 | 1034 | 1041 | 1048 | 1055 | 1064 | 25 Ω 1069 | 1074 | 1081 | 1090 | 50 Ω 1095 | 1102 | 1111 | 1118 | 1125 | 1132 | 1141 | 6 1146 | Output 1151 | 1160 | 4 1165 | VS 1178 | 1179 | 1180 | Q1 1185 | Q8 1190 | Q9 1195 | Q12 1200 | Q13 1205 | Q14 1210 | Q17 1215 | Q20 1220 | Q2 1225 | Q3 1230 | Q4 1235 | Q7 1240 | Q5 1245 | Q6 1250 | Q10 1255 | Q11 1260 | Q22 1265 | Q15 1270 | Q19 1275 | Q16 1280 | 1281 | --------------------------------------------------------------------------------