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.
588 lines
15 KiB
588 lines
15 KiB
/** |
|
* Copyright (c) 2015-present, Facebook, Inc. |
|
* |
|
* This source code is licensed under the MIT license found in the |
|
* LICENSE file in the root directory of this source tree. |
|
* |
|
* @providesModule ReactNativeART |
|
*/ |
|
'use strict'; |
|
|
|
var Color = require('art/core/color'); |
|
var Path = require('ARTSerializablePath'); |
|
var Transform = require('art/core/transform'); |
|
|
|
var React = require('React'); |
|
var PropTypes = require('prop-types'); |
|
var ReactNativeViewAttributes = require('ReactNativeViewAttributes'); |
|
|
|
var createReactNativeComponentClass = require('createReactNativeComponentClass'); |
|
var merge = require('merge'); |
|
var invariant = require('fbjs/lib/invariant'); |
|
|
|
// Diff Helpers |
|
|
|
function arrayDiffer(a, b) { |
|
if (a == null || b == null) { |
|
return true; |
|
} |
|
if (a.length !== b.length) { |
|
return true; |
|
} |
|
for (var i = 0; i < a.length; i++) { |
|
if (a[i] !== b[i]) { |
|
return true; |
|
} |
|
} |
|
return false; |
|
} |
|
|
|
function fontAndLinesDiffer(a, b) { |
|
if (a === b) { |
|
return false; |
|
} |
|
if (a.font !== b.font) { |
|
if (a.font === null) { |
|
return true; |
|
} |
|
if (b.font === null) { |
|
return true; |
|
} |
|
|
|
if ( |
|
a.font.fontFamily !== b.font.fontFamily || |
|
a.font.fontSize !== b.font.fontSize || |
|
a.font.fontWeight !== b.font.fontWeight || |
|
a.font.fontStyle !== b.font.fontStyle |
|
) { |
|
return true; |
|
} |
|
} |
|
return arrayDiffer(a.lines, b.lines); |
|
} |
|
|
|
// Native Attributes |
|
|
|
var SurfaceViewAttributes = merge(ReactNativeViewAttributes.UIView, { |
|
// This should contain pixel information such as width, height and |
|
// resolution to know what kind of buffer needs to be allocated. |
|
// Currently we rely on UIViews and style to figure that out. |
|
}); |
|
|
|
var NodeAttributes = { |
|
transform: { diff: arrayDiffer }, |
|
opacity: true, |
|
}; |
|
|
|
var GroupAttributes = merge(NodeAttributes, { |
|
clipping: { diff: arrayDiffer } |
|
}); |
|
|
|
var RenderableAttributes = merge(NodeAttributes, { |
|
fill: { diff: arrayDiffer }, |
|
stroke: { diff: arrayDiffer }, |
|
strokeWidth: true, |
|
strokeCap: true, |
|
strokeJoin: true, |
|
strokeDash: { diff: arrayDiffer }, |
|
}); |
|
|
|
var ShapeAttributes = merge(RenderableAttributes, { |
|
d: { diff: arrayDiffer }, |
|
}); |
|
|
|
var TextAttributes = merge(RenderableAttributes, { |
|
alignment: true, |
|
frame: { diff: fontAndLinesDiffer }, |
|
path: { diff: arrayDiffer } |
|
}); |
|
|
|
// Native Components |
|
|
|
var NativeSurfaceView = createReactNativeComponentClass('ARTSurfaceView', |
|
() => ({ |
|
validAttributes: SurfaceViewAttributes, |
|
uiViewClassName: 'ARTSurfaceView', |
|
})); |
|
|
|
var NativeGroup = createReactNativeComponentClass('ARTGroup', |
|
() => ({ |
|
validAttributes: GroupAttributes, |
|
uiViewClassName: 'ARTGroup', |
|
})); |
|
|
|
var NativeShape = createReactNativeComponentClass('ARTShape', |
|
() => ({ |
|
validAttributes: ShapeAttributes, |
|
uiViewClassName: 'ARTShape', |
|
})); |
|
|
|
var NativeText = createReactNativeComponentClass('ARTText', |
|
() => ({ |
|
validAttributes: TextAttributes, |
|
uiViewClassName: 'ARTText', |
|
})); |
|
|
|
// Utilities |
|
|
|
function childrenAsString(children) { |
|
if (!children) { |
|
return ''; |
|
} |
|
if (typeof children === 'string') { |
|
return children; |
|
} |
|
if (children.length) { |
|
return children.join('\n'); |
|
} |
|
return ''; |
|
} |
|
|
|
// Surface - Root node of all ART |
|
|
|
class Surface extends React.Component { |
|
static childContextTypes = { |
|
isInSurface: PropTypes.bool, |
|
}; |
|
|
|
getChildContext() { |
|
return { isInSurface: true }; |
|
} |
|
|
|
render() { |
|
var props = this.props; |
|
var w = extractNumber(props.width, 0); |
|
var h = extractNumber(props.height, 0); |
|
return ( |
|
<NativeSurfaceView style={[props.style, { width: w, height: h }]}> |
|
{this.props.children} |
|
</NativeSurfaceView> |
|
); |
|
} |
|
} |
|
|
|
// Node Props |
|
|
|
// TODO: The desktop version of ART has title and cursor. We should have |
|
// accessibility support here too even though hovering doesn't work. |
|
|
|
function extractNumber(value, defaultValue) { |
|
if (value == null) { |
|
return defaultValue; |
|
} |
|
return +value; |
|
} |
|
|
|
var pooledTransform = new Transform(); |
|
|
|
function extractTransform(props) { |
|
var scaleX = props.scaleX != null ? props.scaleX : |
|
props.scale != null ? props.scale : 1; |
|
var scaleY = props.scaleY != null ? props.scaleY : |
|
props.scale != null ? props.scale : 1; |
|
|
|
pooledTransform |
|
.transformTo(1, 0, 0, 1, 0, 0) |
|
.move(props.x || 0, props.y || 0) |
|
.rotate(props.rotation || 0, props.originX, props.originY) |
|
.scale(scaleX, scaleY, props.originX, props.originY); |
|
|
|
if (props.transform != null) { |
|
pooledTransform.transform(props.transform); |
|
} |
|
|
|
return [ |
|
pooledTransform.xx, pooledTransform.yx, |
|
pooledTransform.xy, pooledTransform.yy, |
|
pooledTransform.x, pooledTransform.y, |
|
]; |
|
} |
|
|
|
function extractOpacity(props) { |
|
// TODO: visible === false should also have no hit detection |
|
if (props.visible === false) { |
|
return 0; |
|
} |
|
if (props.opacity == null) { |
|
return 1; |
|
} |
|
return +props.opacity; |
|
} |
|
|
|
// Groups |
|
|
|
// Note: ART has a notion of width and height on Group but AFAIK it's a noop in |
|
// ReactART. |
|
|
|
class Group extends React.Component { |
|
static contextTypes = { |
|
isInSurface: PropTypes.bool.isRequired, |
|
}; |
|
|
|
render() { |
|
var props = this.props; |
|
invariant( |
|
this.context.isInSurface, |
|
'ART: <Group /> must be a child of a <Surface />' |
|
); |
|
return ( |
|
<NativeGroup |
|
opacity={extractOpacity(props)} |
|
transform={extractTransform(props)}> |
|
{this.props.children} |
|
</NativeGroup> |
|
); |
|
} |
|
} |
|
|
|
class ClippingRectangle extends React.Component { |
|
render() { |
|
var props = this.props; |
|
var x = extractNumber(props.x, 0); |
|
var y = extractNumber(props.y, 0); |
|
var w = extractNumber(props.width, 0); |
|
var h = extractNumber(props.height, 0); |
|
var clipping = [x, y, w, h]; |
|
// The current clipping API requires x and y to be ignored in the transform |
|
var propsExcludingXAndY = merge(props); |
|
delete propsExcludingXAndY.x; |
|
delete propsExcludingXAndY.y; |
|
return ( |
|
<NativeGroup |
|
clipping={clipping} |
|
opacity={extractOpacity(props)} |
|
transform={extractTransform(propsExcludingXAndY)}> |
|
{this.props.children} |
|
</NativeGroup> |
|
); |
|
} |
|
} |
|
|
|
// Renderables |
|
|
|
var SOLID_COLOR = 0; |
|
var LINEAR_GRADIENT = 1; |
|
var RADIAL_GRADIENT = 2; |
|
var PATTERN = 3; |
|
|
|
function insertColorIntoArray(color, targetArray, atIndex) { |
|
var c = new Color(color); |
|
targetArray[atIndex + 0] = c.red / 255; |
|
targetArray[atIndex + 1] = c.green / 255; |
|
targetArray[atIndex + 2] = c.blue / 255; |
|
targetArray[atIndex + 3] = c.alpha; |
|
} |
|
|
|
function insertColorsIntoArray(stops, targetArray, atIndex) { |
|
var i = 0; |
|
if ('length' in stops) { |
|
while (i < stops.length) { |
|
insertColorIntoArray(stops[i], targetArray, atIndex + i * 4); |
|
i++; |
|
} |
|
} else { |
|
for (var offset in stops) { |
|
insertColorIntoArray(stops[offset], targetArray, atIndex + i * 4); |
|
i++; |
|
} |
|
} |
|
return atIndex + i * 4; |
|
} |
|
|
|
function insertOffsetsIntoArray(stops, targetArray, atIndex, multi, reverse) { |
|
var offsetNumber; |
|
var i = 0; |
|
if ('length' in stops) { |
|
while (i < stops.length) { |
|
offsetNumber = i / (stops.length - 1) * multi; |
|
targetArray[atIndex + i] = reverse ? 1 - offsetNumber : offsetNumber; |
|
i++; |
|
} |
|
} else { |
|
for (var offsetString in stops) { |
|
offsetNumber = (+offsetString) * multi; |
|
targetArray[atIndex + i] = reverse ? 1 - offsetNumber : offsetNumber; |
|
i++; |
|
} |
|
} |
|
return atIndex + i; |
|
} |
|
|
|
function insertColorStopsIntoArray(stops, targetArray, atIndex) { |
|
var lastIndex = insertColorsIntoArray(stops, targetArray, atIndex); |
|
insertOffsetsIntoArray(stops, targetArray, lastIndex, 1, false); |
|
} |
|
|
|
function insertDoubleColorStopsIntoArray(stops, targetArray, atIndex) { |
|
var lastIndex = insertColorsIntoArray(stops, targetArray, atIndex); |
|
lastIndex = insertColorsIntoArray(stops, targetArray, lastIndex); |
|
lastIndex = insertOffsetsIntoArray(stops, targetArray, lastIndex, 0.5, false); |
|
insertOffsetsIntoArray(stops, targetArray, lastIndex, 0.5, true); |
|
} |
|
|
|
function applyBoundingBoxToBrushData(brushData, props) { |
|
var type = brushData[0]; |
|
var width = +props.width; |
|
var height = +props.height; |
|
if (type === LINEAR_GRADIENT) { |
|
brushData[1] *= width; |
|
brushData[2] *= height; |
|
brushData[3] *= width; |
|
brushData[4] *= height; |
|
} else if (type === RADIAL_GRADIENT) { |
|
brushData[1] *= width; |
|
brushData[2] *= height; |
|
brushData[3] *= width; |
|
brushData[4] *= height; |
|
brushData[5] *= width; |
|
brushData[6] *= height; |
|
} else if (type === PATTERN) { |
|
// todo |
|
} |
|
} |
|
|
|
function extractBrush(colorOrBrush, props) { |
|
if (colorOrBrush == null) { |
|
return null; |
|
} |
|
if (colorOrBrush._brush) { |
|
if (colorOrBrush._bb) { |
|
// The legacy API for Gradients allow for the bounding box to be used |
|
// as a convenience for specifying gradient positions. This should be |
|
// deprecated. It's not properly implemented in canvas mode. ReactART |
|
// doesn't handle update to the bounding box correctly. That's why we |
|
// mutate this so that if it's reused, we reuse the same resolved box. |
|
applyBoundingBoxToBrushData(colorOrBrush._brush, props); |
|
colorOrBrush._bb = false; |
|
} |
|
return colorOrBrush._brush; |
|
} |
|
var c = new Color(colorOrBrush); |
|
return [SOLID_COLOR, c.red / 255, c.green / 255, c.blue / 255, c.alpha]; |
|
} |
|
|
|
function extractColor(color) { |
|
if (color == null) { |
|
return null; |
|
} |
|
var c = new Color(color); |
|
return [c.red / 255, c.green / 255, c.blue / 255, c.alpha]; |
|
} |
|
|
|
function extractStrokeCap(strokeCap) { |
|
switch (strokeCap) { |
|
case 'butt': return 0; |
|
case 'square': return 2; |
|
default: return 1; // round |
|
} |
|
} |
|
|
|
function extractStrokeJoin(strokeJoin) { |
|
switch (strokeJoin) { |
|
case 'miter': return 0; |
|
case 'bevel': return 2; |
|
default: return 1; // round |
|
} |
|
} |
|
|
|
// Shape |
|
|
|
// Note: ART has a notion of width and height on Shape but AFAIK it's a noop in |
|
// ReactART. |
|
|
|
class Shape extends React.Component { |
|
render() { |
|
var props = this.props; |
|
var path = props.d || childrenAsString(props.children); |
|
var d = (path instanceof Path ? path : new Path(path)).toJSON(); |
|
return ( |
|
<NativeShape |
|
fill={extractBrush(props.fill, props)} |
|
opacity={extractOpacity(props)} |
|
stroke={extractColor(props.stroke)} |
|
strokeCap={extractStrokeCap(props.strokeCap)} |
|
strokeDash={props.strokeDash || null} |
|
strokeJoin={extractStrokeJoin(props.strokeJoin)} |
|
strokeWidth={extractNumber(props.strokeWidth, 1)} |
|
transform={extractTransform(props)} |
|
|
|
d={d} |
|
/> |
|
); |
|
} |
|
} |
|
|
|
// Text |
|
|
|
var cachedFontObjectsFromString = {}; |
|
|
|
var fontFamilyPrefix = /^[\s"']*/; |
|
var fontFamilySuffix = /[\s"']*$/; |
|
|
|
function extractSingleFontFamily(fontFamilyString) { |
|
// ART on the web allows for multiple font-families to be specified. |
|
// For compatibility, we extract the first font-family, hoping |
|
// we'll get a match. |
|
return fontFamilyString.split(',')[0] |
|
.replace(fontFamilyPrefix, '') |
|
.replace(fontFamilySuffix, ''); |
|
} |
|
|
|
function parseFontString(font) { |
|
if (cachedFontObjectsFromString.hasOwnProperty(font)) { |
|
return cachedFontObjectsFromString[font]; |
|
} |
|
var regexp = /^\s*((?:(?:normal|bold|italic)\s+)*)(?:(\d+(?:\.\d+)?)[ptexm\%]*(?:\s*\/.*?)?\s+)?\s*\"?([^\"]*)/i; |
|
var match = regexp.exec(font); |
|
if (!match) { |
|
return null; |
|
} |
|
var fontFamily = extractSingleFontFamily(match[3]); |
|
var fontSize = +match[2] || 12; |
|
var isBold = /bold/.exec(match[1]); |
|
var isItalic = /italic/.exec(match[1]); |
|
cachedFontObjectsFromString[font] = { |
|
fontFamily: fontFamily, |
|
fontSize: fontSize, |
|
fontWeight: isBold ? 'bold' : 'normal', |
|
fontStyle: isItalic ? 'italic' : 'normal', |
|
}; |
|
return cachedFontObjectsFromString[font]; |
|
} |
|
|
|
function extractFont(font) { |
|
if (font == null) { |
|
return null; |
|
} |
|
if (typeof font === 'string') { |
|
return parseFontString(font); |
|
} |
|
var fontFamily = extractSingleFontFamily(font.fontFamily); |
|
var fontSize = +font.fontSize || 12; |
|
var fontWeight = font.fontWeight != null ? font.fontWeight.toString() : '400'; |
|
return { |
|
// Normalize |
|
fontFamily: fontFamily, |
|
fontSize: fontSize, |
|
fontWeight: fontWeight, |
|
fontStyle: font.fontStyle, |
|
}; |
|
} |
|
|
|
var newLine = /\n/g; |
|
function extractFontAndLines(font, text) { |
|
return { font: extractFont(font), lines: text.split(newLine) }; |
|
} |
|
|
|
function extractAlignment(alignment) { |
|
switch (alignment) { |
|
case 'right': |
|
return 1; |
|
case 'center': |
|
return 2; |
|
default: |
|
return 0; |
|
} |
|
} |
|
|
|
class Text extends React.Component { |
|
render() { |
|
var props = this.props; |
|
var path = props.path; |
|
var textPath = path ? (path instanceof Path ? path : new Path(path)).toJSON() : null; |
|
var textFrame = extractFontAndLines( |
|
props.font, |
|
childrenAsString(props.children) |
|
); |
|
return ( |
|
<NativeText |
|
fill={extractBrush(props.fill, props)} |
|
opacity={extractOpacity(props)} |
|
stroke={extractColor(props.stroke)} |
|
strokeCap={extractStrokeCap(props.strokeCap)} |
|
strokeDash={props.strokeDash || null} |
|
strokeJoin={extractStrokeJoin(props.strokeJoin)} |
|
strokeWidth={extractNumber(props.strokeWidth, 1)} |
|
transform={extractTransform(props)} |
|
|
|
alignment={extractAlignment(props.alignment)} |
|
frame={textFrame} |
|
path={textPath} |
|
/> |
|
); |
|
} |
|
} |
|
|
|
// Declarative fill type objects - API design not finalized |
|
|
|
function LinearGradient(stops, x1, y1, x2, y2) { |
|
var type = LINEAR_GRADIENT; |
|
|
|
if (arguments.length < 5) { |
|
var angle = ((x1 == null) ? 270 : x1) * Math.PI / 180; |
|
|
|
var x = Math.cos(angle); |
|
var y = -Math.sin(angle); |
|
var l = (Math.abs(x) + Math.abs(y)) / 2; |
|
|
|
x *= l; y *= l; |
|
|
|
x1 = 0.5 - x; |
|
x2 = 0.5 + x; |
|
y1 = 0.5 - y; |
|
y2 = 0.5 + y; |
|
this._bb = true; |
|
} else { |
|
this._bb = false; |
|
} |
|
|
|
var brushData = [type, +x1, +y1, +x2, +y2]; |
|
insertColorStopsIntoArray(stops, brushData, 5); |
|
this._brush = brushData; |
|
} |
|
|
|
function RadialGradient(stops, fx, fy, rx, ry, cx, cy) { |
|
if (ry == null) { |
|
ry = rx; |
|
} |
|
if (cx == null) { |
|
cx = fx; |
|
} |
|
if (cy == null) { |
|
cy = fy; |
|
} |
|
if (fx == null) { |
|
// As a convenience we allow the whole radial gradient to cover the |
|
// bounding box. We should consider dropping this API. |
|
fx = fy = rx = ry = cx = cy = 0.5; |
|
this._bb = true; |
|
} else { |
|
this._bb = false; |
|
} |
|
// The ART API expects the radial gradient to be repeated at the edges. |
|
// To simulate this we render the gradient twice as large and add double |
|
// color stops. Ideally this API would become more restrictive so that this |
|
// extra work isn't needed. |
|
var brushData = [RADIAL_GRADIENT, +fx, +fy, +rx * 2, +ry * 2, +cx, +cy]; |
|
insertDoubleColorStopsIntoArray(stops, brushData, 7); |
|
this._brush = brushData; |
|
} |
|
|
|
function Pattern(url, width, height, left, top) { |
|
this._brush = [PATTERN, url, +left || 0, +top || 0, +width, +height]; |
|
} |
|
|
|
var ReactART = { |
|
LinearGradient: LinearGradient, |
|
RadialGradient: RadialGradient, |
|
Pattern: Pattern, |
|
Transform: Transform, |
|
Path: Path, |
|
Surface: Surface, |
|
Group: Group, |
|
ClippingRectangle: ClippingRectangle, |
|
Shape: Shape, |
|
Text: Text, |
|
}; |
|
|
|
module.exports = ReactART;
|
|
|