This app provides monitoring and information features for the common freifunk user and the technical stuff of a freifunk community.
Code base is taken from a TUM Practical Course project and added here to see if Freifunk Altdorf can use it.
https://www.freifunk-altdorf.de
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
417 lines
13 KiB
417 lines
13 KiB
var Class = require('../../core/class'); |
|
var Color = require('../../core/color'); |
|
var Mode = require('../../modes/current'); |
|
var Rectangle = require('../../shapes/rectangle'); |
|
|
|
// Regular Expressions |
|
|
|
var matchURL = /^\s*url\(["'\s]*([^\)]*?)["'\s]*\)/, |
|
requiredNumber = '(?:\\s+|\\s*,\\s*)([^\\s,\\)]+)'; |
|
number = '(?:' + requiredNumber + ')?', |
|
matchViewBox = new RegExp('^\\s*([^\\s,]+)' + requiredNumber + requiredNumber + requiredNumber), |
|
matchUnit = /^\s*([\+\-\d\.]+(?:e\d+)?)(|px|em|ex|in|pt|pc|mm|cm|%)\s*$/i; |
|
|
|
// Environment Settings |
|
var dpi = 72, emToEx = 0.5; |
|
|
|
var styleSheet = function(){}, |
|
defaultStyles = { |
|
'viewportWidth': 500, |
|
'viewportHeight': 500, |
|
'font-family': 'Arial', |
|
'font-size': 12, |
|
'color': 'black', |
|
'fill': 'black' |
|
}, |
|
nonInheritedStyles = { |
|
'stop-color': 'black', |
|
'stop-opacity': 1, |
|
'clip-path': null, |
|
'filter': null, |
|
'mask': null, |
|
'opacity': 1, |
|
'cursor': null |
|
}; |
|
|
|
// Visitor |
|
|
|
var SVGParser = Class({ |
|
|
|
initialize: function(mode){ |
|
this.MODE = mode; |
|
}, |
|
|
|
// TODO Fix this silly API |
|
parseAsSurface: function(element, styles){ |
|
return this.parse(element, styles, true); |
|
}, |
|
|
|
parse: function(element, styles, asSurface){ |
|
if (typeof element == 'string') element = this.parseXML(element); |
|
|
|
if (!styles) |
|
styles = this.findStyles(element); |
|
else |
|
for (var style in defaultStyles) |
|
if (!(style in styles)) |
|
styles[style] = defaultStyles[style]; |
|
|
|
if (element.documentElement || asSurface){ |
|
element = element.documentElement || element; |
|
var canvas = new this.MODE.Surface( |
|
this.parseLength(element.getAttribute('width') || '100%', styles, 'x'), |
|
this.parseLength(element.getAttribute('height') || '100%', styles, 'y') |
|
); |
|
if (element.getAttribute('viewBox')) |
|
canvas.grab(this.parse(element, styles)); |
|
else |
|
this.container(element, this.parseStyles(element, styles), canvas); |
|
return canvas; |
|
} |
|
if (element.nodeType != 1 || element.getAttribute('requiredExtensions') || element.getAttribute('systemLanguage') != null) return null; |
|
styles = this.parseStyles(element, styles); |
|
var parseFunction = this[element.nodeName + 'Element']; |
|
return parseFunction ? parseFunction.call(this, element, styles) : null; |
|
}, |
|
|
|
parseXML: window.DOMParser ? function(text){ |
|
return new DOMParser().parseFromString(text, 'text/xml'); |
|
} : function(text){ |
|
try { |
|
var xml; |
|
try { xml = new ActiveXObject('MSXML2.DOMDocument'); } |
|
catch (e){ xml = new ActiveXObject('Microsoft.XMLDOM'); } |
|
xml.resolveExternals = false; |
|
xml.validateOnParse = false; |
|
xml.async = false; |
|
xml.preserveWhiteSpace = true; |
|
xml.loadXML(text); |
|
return xml; |
|
} catch (e){ |
|
return null; |
|
} |
|
}, |
|
|
|
parseStyles: function(element, styles){ |
|
styleSheet.prototype = styles; |
|
var newSheet = new styleSheet(); |
|
for (var key in nonInheritedStyles) newSheet[key] = nonInheritedStyles[key]; |
|
this.applyStyles(element, newSheet); |
|
if (newSheet.hasOwnProperty('font-size')){ |
|
var newFontSize = this.parseLength(newSheet['font-size'], styles, 'font'); |
|
if (newFontSize != null) newSheet['font-size'] = newFontSize; |
|
} |
|
if (newSheet.hasOwnProperty('text-decoration')){ |
|
newSheet['text-decoration-color'] = newSheet.color; |
|
} |
|
return newSheet; |
|
}, |
|
|
|
findStyles: function(element){ |
|
if (!element || element.nodeType != 1) return defaultStyles; |
|
var styles = this.findStyles(element.parentNode); |
|
return this.parseStyles(element, styles); |
|
}, |
|
|
|
applyStyles: function(element, target){ |
|
var attributes = element.attributes; |
|
for (var i = 0, l = attributes.length; i < l; i++){ |
|
var attribute = attributes[i], |
|
name = attribute.nodeName, |
|
value = attribute.nodeValue; |
|
if (value != 'inherit'){ |
|
target[name] = value; |
|
if (name == 'fill') target['fill_document'] = element.ownerDocument; |
|
} |
|
} |
|
return target; |
|
}, |
|
|
|
findById: function(document, id){ |
|
// if (document.getElementById) return document.getElementById(id); Not reliable |
|
if (this.cacheDocument != document){ |
|
this.ids = {}; |
|
this.lastSweep = null; |
|
this.cacheDocument = document; |
|
} |
|
var ids = this.ids; |
|
if (ids[id] != null) return ids[id]; |
|
var root = document.documentElement, node = this.lastSweep || root; |
|
treewalker: while (node){ |
|
if (node.nodeType == 1){ |
|
var newID = node.getAttribute('id') || node.getAttribute('xml:id'); |
|
if (newID && ids[newID] == null) ids[newID] = node; |
|
if (newID == id){ |
|
this.lastSweep = node; |
|
return node; |
|
} |
|
} |
|
if (node.firstChild){ |
|
node = node.firstChild; |
|
} else { |
|
while (!node.nextSibling){ |
|
node = node.parentNode; |
|
if (!node || node == root) break treewalker; |
|
} |
|
node = node.nextSibling; |
|
} |
|
} |
|
return null; |
|
}, |
|
|
|
findByURL: function(document, url, callback){ |
|
callback.call(this, url && url[0] == '#' ? this.findById(document, url.substr(1)) : null); |
|
}, |
|
|
|
resolveURL: function(url){ |
|
return url; |
|
}, |
|
|
|
parseLength: function(value, styles, dimension){ |
|
var match = matchUnit.exec(value); |
|
if (!match) return null; |
|
var result = parseFloat(match[1]); |
|
switch(match[2]){ |
|
case '': case 'px': return result; |
|
case 'em': return result * styles['font-size']; |
|
case 'ex': return result * styles['font-size'] * emToEx; |
|
case 'in': return result * dpi; |
|
case 'pt': return result * dpi / 72; |
|
case 'pc': return result * dpi / 6; |
|
case 'mm': return result * dpi / 25.4; |
|
case 'cm': return result * dpi / 2.54; |
|
case '%': |
|
var w = styles.viewportWidth, h = styles.viewportHeight; |
|
if (dimension == 'font') return result * styles['font-size'] / 100; |
|
if (dimension == 'x') return result * w / 100; |
|
if (dimension == 'y') return result * h / 100; |
|
return result * Math.sqrt(w * w + h * h) / Math.sqrt(2) / 100; |
|
} |
|
}, |
|
|
|
parseColor: function(value, opacity, styles){ |
|
if (value == 'currentColor') value = styles.color; |
|
try { |
|
var color = new Color(value); |
|
} catch (x){ |
|
// Ignore unparsable colors, TODO: log |
|
return null; |
|
} |
|
color.alpha = opacity == null ? 1 : +opacity; |
|
return color; |
|
}, |
|
|
|
getLengthAttribute: function(element, styles, attr, dimension){ |
|
return this.parseLength(element.getAttribute(attr) || 0, styles, dimension); |
|
}, |
|
|
|
container: function(element, styles, container){ |
|
if (container.width != null) styles.viewportWidth = container.width; |
|
if (container.height != null) styles.viewportHeight = container.height; |
|
this.filter(styles, container); |
|
this.describe(element, styles, container); |
|
var node = element.firstChild; |
|
while (node){ |
|
var art = this.parse(node, styles); |
|
if (art) container.grab(art); |
|
node = node.nextSibling; |
|
} |
|
return container; |
|
}, |
|
|
|
shape: function(element, styles, target, x, y){ |
|
this.transform(element, target); |
|
target.transform(1, 0, 0, 1, x, y); |
|
this.fill(styles, target, x, y); |
|
this.stroke(styles, target); |
|
this.filter(styles, target); |
|
if (styles.visibility == 'hidden') target.hide(); |
|
this.describe(element, styles, target); |
|
return target; |
|
}, |
|
|
|
fill: function(styles, target, x, y){ |
|
if (!styles.fill || styles.fill == 'none') return; |
|
var match; |
|
if (match = matchURL.exec(styles.fill)){ |
|
this.findByURL(styles.fill_document, match[1], function(fill){ |
|
var fillFunction = fill && this[fill.nodeName + 'Fill']; |
|
if (fillFunction) fillFunction.call(this, fill, this.findStyles(fill), target, x, y); |
|
}); |
|
} else { |
|
target.fill(this.parseColor(styles.fill, styles['fill-opacity'], styles)); |
|
} |
|
}, |
|
|
|
stroke: function(styles, target){ |
|
if (!styles.stroke || styles.stroke == 'none' || matchURL.test(styles.stroke)) return; // Advanced stroke colors are not supported, TODO: log |
|
var color = this.parseColor(styles.stroke, styles['stroke-opacity'], styles), |
|
width = this.parseLength(styles['stroke-width'], styles), |
|
cap = styles['stroke-linecap'] || 'butt', |
|
join = styles['stroke-linejoin'] || 'miter'; |
|
target.stroke(color, width == null ? 1 : width, cap, join); |
|
}, |
|
|
|
filter: function(styles, target){ |
|
if (styles.opacity != 1 && target.blend) target.blend(styles.opacity); |
|
if (styles.display == 'none') target.hide(); |
|
}, |
|
|
|
describe: function(element, styles, target){ |
|
var node = element.firstChild, title = ''; |
|
if (element.nodeName != 'svg') |
|
while (node){ |
|
if (node.nodeName == 'title') title += node.firstChild && node.firstChild.nodeValue; |
|
node = node.nextSibling; |
|
} |
|
if (styles.cursor || title) target.indicate(styles.cursor, title); |
|
}, |
|
|
|
transform: function(element, target){ |
|
var transform = element.getAttribute('transform'), match; |
|
var matchTransform = new RegExp('([a-z]+)\\s*\\(\\s*([^\\s,\\)]+)' + number + number + number + number + number + '\\s*\\)', 'gi'); |
|
while(match = transform && matchTransform.exec(transform)){ |
|
switch(match[1]){ |
|
case 'matrix': |
|
target.transform(match[2], match[3], match[4], match[5], match[6], match[7]); |
|
break; |
|
case 'translate': |
|
target.transform(1, 0, 0, 1, match[2], match[3]); |
|
break; |
|
case 'scale': |
|
target.transform(match[2], 0, 0, match[3] || match[2]); |
|
break; |
|
case 'rotate': |
|
var rad = match[2] * Math.PI / 180, cos = Math.cos(rad), sin = Math.sin(rad); |
|
target.transform(1, 0, 0, 1, match[3], match[4]) |
|
.transform(cos, sin, -sin, cos) |
|
.transform(1, 0, 0, 1, -match[3], -match[4]); |
|
break; |
|
case 'skewX': |
|
target.transform(1, 0, Math.tan(match[2] * Math.PI / 180), 1); |
|
break; |
|
case 'skewY': |
|
target.transform(1, Math.tan(match[2] * Math.PI / 180), 0, 1); |
|
break; |
|
} |
|
} |
|
}, |
|
|
|
svgElement: function(element, styles){ |
|
var viewbox = element.getAttribute('viewBox'), |
|
match = matchViewBox.exec(viewbox), |
|
x = this.getLengthAttribute(element, styles, 'x', 'x'), |
|
y = this.getLengthAttribute(element, styles, 'y', 'y'), |
|
width = this.getLengthAttribute(element, styles, 'width', 'x'), |
|
height = this.getLengthAttribute(element, styles, 'height', 'y'), |
|
group = match ? new this.MODE.Group(+match[3], +match[4]) : new this.MODE.Group(width || null, height || null); |
|
if (width && height) group.resizeTo(width, height); // TODO: Aspect ratio |
|
if (match) group.transform(1, 0, 0, 1, -match[1], -match[2]); |
|
this.container(element, styles, group); |
|
group.move(x, y); |
|
return group; |
|
}, |
|
|
|
gElement: function(element, styles){ |
|
var group = new this.MODE.Group(); |
|
this.transform(element, group); |
|
this.container(element, styles, group); |
|
return group; |
|
}, |
|
|
|
useElement: function(element, styles){ |
|
var placeholder = new this.MODE.Group(), |
|
x = this.getLengthAttribute(element, styles, 'x', 'x'), |
|
y = this.getLengthAttribute(element, styles, 'y', 'y'), |
|
width = this.getLengthAttribute(element, styles, 'width', 'x'), |
|
height = this.getLengthAttribute(element, styles, 'height', 'y'); |
|
|
|
this.transform(element, placeholder); |
|
placeholder.transform(1, 0, 0, 1, x, y); |
|
|
|
this.findByURL(element.ownerDocument, element.getAttribute('xlink:href') || element.getAttribute('href'), function(target){ |
|
if (!target || target.nodeType != 1) return; |
|
|
|
var parseFunction = target.nodeName == 'symbol' ? this.svgElement : this[target.nodeName + 'Element']; |
|
if (!parseFunction) return; |
|
|
|
styles = this.parseStyles(element, this.parseStyles(target, styles)); |
|
|
|
var symbol = parseFunction.call(this, target, styles); |
|
if (!symbol) return; |
|
if (width && height) symbol.resizeTo(width, height); // TODO: Aspect ratio, maybe resize the placeholder instead |
|
placeholder.grab(symbol); |
|
}); |
|
|
|
return placeholder; |
|
}, |
|
|
|
switchElement: function(element, styles){ |
|
var node = element.firstChild; |
|
while (node){ |
|
var art = this.parse(node, styles); |
|
if (art) return art; |
|
node = node.nextSibling; |
|
} |
|
return null; |
|
}, |
|
|
|
aElement: function(element, styles){ |
|
// For now treat it like a group |
|
return this.gElement(element, styles); |
|
}, |
|
|
|
pathElement: function(element, styles){ |
|
var shape = new this.MODE.Shape(element.getAttribute('d') || null); |
|
this.shape(element, styles, shape); |
|
return shape; |
|
}, |
|
|
|
imageElement: function(element, styles){ |
|
var href = this.resolveURL(element.getAttribute('xlink:href') || element.getAttribute('href')), |
|
width = this.getLengthAttribute(element, styles, 'width', 'x'), |
|
height = this.getLengthAttribute(element, styles, 'height', 'y'), |
|
x = this.getLengthAttribute(element, styles, 'x', 'x'), |
|
y = this.getLengthAttribute(element, styles, 'y', 'y'), |
|
clipPath = element.getAttribute('clip-path'), |
|
image, |
|
match; |
|
|
|
if (clipPath && (match = matchURL.exec(clipPath)) && match[1][0] == '#'){ |
|
var clip = this.findById(element.ownerDocument, match[1].substr(1)); |
|
if (clip){ |
|
image = this.switchElement(clip, styles); |
|
if (image){ |
|
if (typeof image.fillImage == 'function'){ |
|
image.fillImage(href, width, height); |
|
if (image.stroke) image.stroke(0); |
|
} else { |
|
image = null; |
|
} |
|
} |
|
} |
|
} |
|
if (!image){ |
|
//image = new Image(href, width, height); TODO |
|
image = new Rectangle(width, height).fillImage(href, width, height); |
|
} |
|
this.filter(styles, image); |
|
if (styles.visibility == 'hidden') target.hide(); |
|
this.describe(element, styles, image); |
|
this.transform(element, image); |
|
image.transform(1, 0, 0, 1, x, y); |
|
return image; |
|
} |
|
|
|
}); |
|
|
|
SVGParser.parse = function(element, styles){ |
|
return new SVGParser(Mode).parse(element, styles); |
|
}; |
|
|
|
SVGParser.implement = function(obj){ |
|
for (var key in obj) |
|
this.prototype[key] = obj[key]; |
|
}; |
|
|
|
module.exports = SVGParser; |