![]() |
Web Development »
Client side scripting »
General
Intermediate
License: The Code Project Open License (CPOL)
Bridge design pattern with JavaScriptBy Dmitry KhudorozhkovThis article explains what the Bridge design pattern is, and presents a minimalistic cross-browser vector graphics (SVG + VML) library, written in JavaScript, built with the help of this pattern. |
Javascript, HTML, XHTML, Windows, Visual Studio, Dev
|
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||
Thanks to those of you who take the time to read through the article. If you feel like dropping off a vote (and particularly if it's a low one), please include a comment which mentions what the problem was. I've been getting mostly high votes for this article, apart from the odd 1's or 2's, and I'd really like to know what bothered those voters the most. Feedback is what drives improvement.
This article assumes you are familiar with object aspects of the JavaScript language. If not, it is recommended that you read the first half of Salvatore Vetro's article.
This article is organized as follows:
Graphic content is the thing every web developer deals with in every single page created. Problems begin when it goes a little further than placing an <IMG> tag. You may wish to build a web-based graphics editor, game, ad banner, interactive teaser, etc.
The first solution that comes to mind is Flash. You can satisfy almost any web graphics need with it, but disadvantages can be prohibitive. They are: need for investments in docs, tools, and programmers; deficient support for in-browser scripting; incompatibility with numerous web media standards, problems with Flash Player security; etc.
XML-based standards are the alternative. They offer perfect web integration (as XML does), are extensible, and do not require downloading large binary archives. The only drawback is that XML is 'still' an emerging technology, so you'll meet numerous incompatibility issues when dealing with XML grammars.
To start with web graphics, we'll need something simple like this:
<script type="text/javascript">
var vgr = new VectorGraphics(...);
vgr.MoveTo(100, 100);
vgr.Rect(100, 100, "#0F0");
vgr.MoveTo(350, 350);
vgr.Circle(50, "#F0F");
...
</script>
As usual, there is more than one way to do the job. To be exact, the number of ways is the number of browsers, plug-ins, and standards (cross-multiplied with each other...) on the web. To satisfy the needs of all (or, at least, the majority of) your customers, you must pass all these ways... Do you want to meet the multiple-page-browser-and-platform-detection-stuff (with all those switches and ifs so easy to get lost in) once again? I don't.
So, before we move on with rectangles and circles, we'll have to answer two not-so-simple questions:
The answer is:
Note: JavaScript is a prototype-based language, and doesn't support many features common to object-oriented languages. Where it makes sense, I'll make notes describing differences and sideways; they are designated as [JavaScript].
Consider the implementation of a cross-browser vector graphics library. Its main purpose is to allow us to use an (X)HTML page as a painting canvas, so we can draw various shapes: lines, rectangles, circles, etc. It also should enable us to write scripts that work on any browser that supports either SVG or VML standards. Using inheritance ([JavaScript]: prototype chaining), we could define an interface VectorGraphics, and classes VectorGraphicsImplSVG and VectorGraphicsImplVML that implement the given interface:

This approach has two problems:
VectorGraphics abstraction to cover more specialized kinds of vector graphics API, or new platforms. Imagine a GeoGraphics subclass of VectorGraphics that specializes the vector graphics to display GIS data. To support GeoGraphics for both SVG and VML user agents, we have to implement two new classes, GeoGraphicsImplSVG and GeoGraphicsImplVML. Worse, we'll have to define two classes for every new XML-based graphics standard. Supporting a third API requires yet another new vector graphics subclass for every specific case of VectorGraphics. Every framework extension leads to the explosive growth of class/prototype hierarchy:

GeoGraphicsImplSVG and GeoGraphicsImplVML, what class should he/she instantiate? It is clear that the VectorGraphics class itself must "decide" what class to present to the end-user. In other words, clients should be able to instantiate a VectorGraphics object without committing to a concrete implementation. Only the implementation classes should depend on the platform on which the script runs. Therefore, client code should instantiate VectorGraphics without mentioning specific graphics standards. The Bridge pattern solves the problem by putting the VectorGraphics abstraction and all its implementations in separate class/prototype hierarchies. There is one hierarchy for VectorGraphics interfaces (VectorGraphics, GeoGraphics, UIGraphics), and a separate hierarchy for platform-specific implementations, with VectorGraphicsImpl as its root (see details below). The VectorGraphicsImplSVG, for example, provides an implementation based on the SVG rendering subsystem.

Such a relationship between VectorGraphics and VectorGraphicsImpl is called a bridge, because it bridges the interface and its implementation, letting them vary independently.
(C++ gurus can say: "bridge pattern adds one 'level of indirection', just like a Proxy pattern".)
Use the Bridge pattern when:

Abstraction RefinedAbstraction Implementor Note: since JavaScript is a loosely-typed language with no notion of abstract classes, Implementor is uncommon to JavaScript implementations of the Bridge pattern. It is usual to have a set of ConcreteImplementors; you set or change the abstraction's implementor reference to one of the ConcreteImplementors during the setup of the bridge. All ConcreteImplementors must conform to a chosen interface, as if all concrete implementors were subclasses of an Implementor.
ConcreteImplementor
Implementor interface and defines its concrete implementation. The abstraction forwards client requests to its Implementor object.
Note: this is not an example script; treat it as a fill-in-form for you to cut-and-paste to your script file, and start coding.
// Abstraction - the object you usually create
// in your "end-user" scripts:
//
// <SCRIPT type="text/javascript">
//
// var abstr = new Abstraction();
// ...
//
// </SCRIPT>
//
function Abstraction()
{
// Implementation reference:
this._impl = null;
// Setup procedure:
this._SetImplementation(this._EstablishImplementor(container));
return this;
}
Abstraction.prototype = {
_SetImplementation: function(implementor)
{
this._impl = null;
if(implementor) this._impl = implementor;
},
// EstablishImplementor - function that creates
// the Concrete Implementor and binds it to Abstraction.
// This is the very method to place your
// browser/feature/object detection code.
_EstablishImplementor: function()
{
if(/*conditionOne*/)
return new ImplementationOne();
else if(/*conditionTwo*/)
return new ImplementationTwo();
// ...
return null;
},
// Function "exported" by the Abstraction:
FuncOne: function()
{
// Check if any implementor is bound and has the required method:
if(_impl && _impl.FuncOne)
_impl.FuncOne(); // Forward request to implementor
}
};
// ...
// This is the first in the set of concrete implementors:
function ImplementationOne()
{
// ...
}
ImplementationOne.prototype = {
// This "public" function is directly called by Abstraction:
FuncOne: function()
{
// ...
},
// ...
}
// This is the second implementor:
function ImplementationTwo()
{
// ...
}
ImplementationTwo.prototype = {
FuncOne: function()
{
// ...
},
// ...
}
Note 1: _EstablishImplementor in the sample above is an example of a Factory Method pattern. We use it to localize the knowledge of which helper class to instantiate.
Note 2: to finish with patterns, I must say that the JavaScript language itself is built upon the Prototype pattern. Each object is derived from a prototype (prototypical instance); object creation is really the operation of cloning the prototype.
Note: the situation with SVG/VML was explained (in short) earlier; this section provides an in-depth review of a problem and presents the solution to it.
There are two well-known standards for web graphics: SVG and VML. Both provide:
Differences:
SVG (Scalable Vector Graphics) - endorsed by W3C, this is truly an industry-wide initiative to bring vector graphics to the web:
Problems are:
Available through:
VML (Vector Markup Language) - another (and, to be honest, the earliest - dates back to 1998) initiative, supported solely by Microsoft:
However:
Available through:
Note: while comparing the lists of contributors within SVG and VML specs, you may notice that these lists overlap. This may signify that Microsoft has, probably, given away VML development in favor of SVG. Nevertheless, VML is still worth looking, as even the newest version (7th, as of July 2007) of Microsoft IE retains native support for it (and doesn't natively support SVG).
For browsers that do not support either of two, an alternative exists: High Performance JavaScript Graphics Library by Walter Zorn. It uses DIVs to draw shapes, so it doesn't rely on any standard.
Here is a high-level API for a VectorGraphics object. Not too extensive, but complete enough to fit the most common drawing needs.
All functions listed below are "public", i.e., intended for use by the client, as opposed to "private" functions (with the preceding underscore), used internally by the library.
// VectorGraphics - the object you should create in your "end-user"
// scripts.
// 'Container' parameter defines an empty DIV or SPAN, which will serve
// as a painting canvas.
//
// <SCRIPT type="text/javascript">
//
// var vg = new VectorGraphics(document.getElementById("container"));
// ...
//
// </SCRIPT>
function VectorGraphics(container)
{ ... }
VectorGraphics.prototype = {
// MoveTo(x, y) - moves current painting position to a point
// with the coordinates of (x, y).
MoveTo: function(x, y)
{ ... },
// LineTo(x, y) - draws a line from current position
// (defined by a previous MoveTo call)
// to a point with the coordinates of (x, y).
// Line is drawn with current stroke color,
// line width is equal to current stroke width.
LineTo: function(x, y)
{ ... },
// Rect(x, y, width, height) - draws a rectangle. Upper-left
// corner is defined by the current position
// (probably set by a previous MoveTo call);
// lower-right corner is:
//
// (currentX + width, currentY + height).
//
// Rectangle is filled with current fill color,
// border is drawn with current stroke color,
// border width is equal to current stroke width.
Rect: function(width, height)
{ ... },
// Ellipse(rx, ry) - draws an ellipse; center
// is defined by the current position
// (probably set by a previous MoveTo call);
// radii are rx and ry. Ellipse is filled with
// current fill color, border is drawn with
// current stroke color, border width is equal
// to current stroke width.
Ellipse: function(rx, ry)
{ ... },
// Polygon(arrayofPoints) - draws a polygon. arrayofPoints
// has the following format:
//
// [[x1, y1],[x2, y2], ... [xn, yn]]
//
// Unlike all other functions, previous MoveTo call
// doesn't define the starting point. Polygon is
// filled with current fill color, border is drawn
// with current stroke color, border width is equal
// to current stroke width.
Polygon: function(arrayofPoints)
{ ... },
// Text(string) - draws a string at the current position
// (probably set by a previous MoveTo call);
// Text is drawn with the current font color,
// and at a current font size.
Text: function(string)
{ ... },
// SetFillColor(rgb) - sets the fill color,
// in #xxx or #xxxxxx format.
SetFillColor: function(rgb)
{ ... },
// SetStrokeColor(rgb) - sets the stroke (line) color,
// in #xxx or #xxxxxx format.
SetStrokeColor: function(rgb)
{ ... },
// SetStrokeWidth(rgb) - sets the stroke (line) width.
SetStrokeWidth: function(rgb)
{ ... },
// SetFont(fontNames, fontSize, fontColor, fontVariant) - sets
// the font family (fontNames), font color (fontColor),
// and font variant (fontVariant, not currently used).
SetFont: function(fontNames, fontSize, fontColor, fontVariant)
{ ... }
}
VectorGraphics is accompanied by two implementors: VectorGraphicsImplSVG and VectorGraphicsImplVML. The implementor is chosen at run-time by the VectorGraphics.prototype._EstablishImplementor method, which is the place where browser detection code is localized. The function simply creates and returns the supported implementor:
// Checks for MSIE + Adobe SVG plugin installation:
VectorGraphics.prototype = {
_CheckASV: function()
{
var adobeplugin = false;
if((navigator.userAgent.toLowerCase().indexOf("msie") != -1) &&
(!window.opera))
{
var obj = null;
try
{
obj = new ActiveXObject("Adobe.SVGCtl");
if(obj)
{
adobeplugin = true;
obj = null;
}
}
catch(e) {};
}
return adobeplugin;
},
_EstablishImplementor: function(container)
{
if((navigator.mimeTypes &&
(navigator.mimeTypes["image/svg+xml"] ... ))
||
// Firefox 1.5+ supports SVG natively:
(navigator.userAgent.toLowerCase().indexOf("firefox")
!= -1) && !window.opera)
return new VectorGraphicsImplSVG(container);
else if(this._CheckASV())
// MSIE + Adobe SVG plugin:
return new VectorGraphicsImplSVG(container);
// MS IE natively supports VML:
if((navigator.userAgent.toLowerCase().indexOf("msie")
!= -1) && !window.opera)
return new VectorGraphicsImplVML(container);
return null;
},
...
}
Note 1: a Batik applet detection code is under development.
Note 2: please take a note that the Netscape browser has neither SVG nor VML support (as of July 2007).
For SVG to be displayed properly, the following conditions must be met:
The _SetupRoot procedure (this name is common to all implementors) performs all the startup tasks needed for an appropriate parser to function properly. For the SVG implementor, it is:
function VectorGraphicsImplSVG()
{
this._svgns = "http://www.w3.org/2000/svg";
this._curX = 0;
this._curY = 0;
this._root = null;
this._cntr = container;
this._prep = null;
return this;
}
VectorGraphicsImplSVG.prototype = {
// MSIE doesn't support createElementNS and
// [get/set]AttributeNS, so we must take care:
//
_createElementNS: function(ns, name)
{
if(document.createElementNS)
return document.createElementNS(ns, this._prep ?
(this._prep + ":" + name) : name)
else
{
var elem = document.createElement(this._prep ?
(this._prep + ":" + name) : name);
elem.setAttribute("xmlns", ns);
return elem;
}
},
_getAttributeNS: function(element, ns, name)
{
return (element.getAttributeNS ?
element.getAttributeNS(ns, name) :
element.getAttribute(name));
},
_setAttributeNS: function(element, ns, name, value)
{
if(element.setAttributeNS)
element.setAttributeNS(ns, name, value);
else
element.setAttribute(name, value);
},
// SVG canvas setup:
_SetupRoot: function()
{
if(this._root) return;
if(VectorGraphics.prototype._CheckASV())
{
var obj = document.createElement("OBJECT");
obj.setAttribute("id", "AdobeSVG");
obj.setAttribute("classid",
"clsid:78156a80-c6a1-4bbf-8e6a-3cd390eeb4e2");
document.getElementsByTagName("head")[0].appendChild(obj);
document.namespaces.add("svg", this._svgns);
document.namespaces("svg").doImport("#AdobeSVG");
this._prep = "svg";
}
this._root = this._createElementNS(this._svgns, "svg");
this._cntr.appendChild(this._root);
this._setAttributeNS(this._root, null, "width", "0");
this._setAttributeNS(this._root, null, "height", "0");
},
...
}
For the SVG code to work, all XML-aware browsers must treat downloaded pages as XHTML, not HTML. The problem is that Microsoft IE doesn't have an XHTML engine; given a document with the XML prologue, it displays just like XML - as a tree of nodes. The dilemma of constructing cross-browser XHTML, however, can be solved:
For VML to show up, the following conditions must be met:
urn:schemas-microsoft-com:vml; behavior: url(#default#VML);; All these tasks are performed by the following _SetupRoot procedure:
function VectorGraphicsImplVML()
{
this._curX = 0;
this._curY = 0;
this._root = null;
this._cntr = container;
return this;
}
VectorGraphicsImplVML.prototype = {
// VML canvas setup:
_SetupRoot: function()
{
if(this._root) return;
// Add VML style definition:
var stl = document.createStyleSheet();
stl.addRule("v\\:*", "behavior: url(#default#VML);");
// Add xml namespace definition:
document.namespaces.add("v",
"urn:schemas-microsoft-com:vml");
var root = document.createElement("v:group");
root.style.width = this._cntr.offsetWidth;
root.style.height = this._cntr.offsetHeight;
root.coordorigin = "0,0";
root.coordsize = this._cntr.offsetWidth + "," +
this._cntr.offsetHeight;
this._root = this._cntr.appendChild(root)
},
...
}
The solution for making XHTML work everywhere is described earlier. Just don't forget about well-formness and other XHTML syntax rules.
Using of the library is simple as nothing:
<script type="text/javascript" src="svg+vml.js"></script>
<script type="text/javascript">
function Draw()
{
var vg = new VectorGraphics(document.getElementById("container"));
vg.SetStrokeColor("black");
vg.SetStrokeWidth(1);
// rectangle #1:
vg.MoveTo(100, 100);
vg.SetFillColor("green");
vg.Rect(100, 100);
// inner squares:
vg.MoveTo(105, 105);
vg.SetFillColor("#ADFF2F");
vg.Rect(10, 10);
vg.MoveTo(120, 105);
vg.SetFillColor("#7CFC00");
vg.Rect(10, 10);
vg.MoveTo(135, 105);
vg.SetFillColor("#32CD32");
vg.Rect(10, 10);
vg.MoveTo(105, 120);
vg.SetFillColor("#3CB371");
vg.Rect(10, 10);
vg.MoveTo(105, 135);
vg.SetFillColor("#2E8B57");
vg.Rect(10, 10);
// green cross:
vg.Polygon([[195,85],[205,85],[205,95],[215,95],[215,105],[205,105],
[205,105],[205,115],[195,115],[195,105],[185,105],
[185,95],[195,95]]);
// supporter lines:
vg.MoveTo(100, 205);
vg.SetStrokeColor("#708090");
vg.LineTo(200, 205);
vg.LineTo(320, 320);
vg.LineTo(543, 320);
vg.SetStrokeColor("black");
vg.SetFont("Arial Unicode MS", 16, "blue", "normal");
// text:
vg.MoveTo(321, 315);
vg.Text("Some Not So Long Blue Text");
// rectangle #2:
vg.MoveTo(100, 240);
vg.SetFillColor("#FF4500");
vg.Rect(100, 100);
vg.MoveTo(105, 245);
vg.SetFillColor("#F08080");
vg.Rect(10, 10);
vg.MoveTo(120, 245);
vg.SetFillColor("#CD5C5C");
vg.Rect(10, 10);
vg.MoveTo(135, 245);
vg.SetFillColor("#FF0000");
vg.Rect(10, 10);
vg.MoveTo(105, 260);
vg.SetFillColor("#B22222");
vg.Rect(10, 10);
vg.MoveTo(105, 275);
vg.SetFillColor("#8B0000");
vg.Rect(10, 10);
// blood cross:
vg.Polygon([[195,225],[205,225],[205,235],
[215,235],[215,245],[205,245],
[205,245],[205,255],[195,255],
[195,245],[185,245],[185,235],[195,235]]);
vg.MoveTo(100, 345);
vg.SetStrokeColor("#90A0B0");
vg.LineTo(200, 345);
vg.LineTo(320, 345);
vg.LineTo(535, 345);
vg.MoveTo(321, 340);
vg.Text("Another Not So Long Blue Text");
vg.SetStrokeColor("black");
// rectangle #3:
vg.MoveTo(100, 400);
vg.SetFillColor("#FFD700");
vg.Rect(100, 100);
vg.MoveTo(105, 405);
vg.SetFillColor("#FFFF00");
vg.Rect(10, 10);
vg.MoveTo(120, 405);
vg.SetFillColor("#FF8C00");
vg.Rect(10, 10);
vg.MoveTo(135, 405);
vg.SetFillColor("#B8860B");
vg.Rect(10, 10);
vg.MoveTo(105, 420);
vg.SetFillColor("#FFA500");
vg.Rect(10, 10);
vg.MoveTo(105, 435);
vg.SetFillColor("#D2691E");
vg.Rect(10, 10);
// yellowish cross:
vg.Polygon([[195,385],[205,385],[205,395],
[215,395],[215,405],[205,405],
[205,405],[205,415],[195,415],
[195,405],[185,405],[185,395],[195,395]]);
vg.MoveTo(100, 505);
vg.SetStrokeColor("#B0C0D0");
vg.LineTo(200, 505);
vg.LineTo(320, 370);
vg.LineTo(542, 370);
vg.MoveTo(321, 365);
vg.Text("Final Not So Long Blue Text");
vg.SetStrokeColor("black");
// circles:
vg.MoveTo(585, 345);
vg.SetFillColor("#DA70D6");
vg.Ellipse(50, 100);
vg.SetFillColor("#BA55D3");
vg.Ellipse(40, 80);
vg.SetFillColor("#9932CC");
vg.Ellipse(30, 60);
vg.SetFillColor("#8B008B");
vg.Ellipse(20, 40);
vg.SetFillColor("#800080");
vg.Ellipse(10, 20);
}
</script>

You may wish to extend the library to support any other API. How much will you have to change to be able to draw with PGML-enabled browsers? Very little:
VectorGraphicsImplPGML object and its methods:
function VectorGraphicsImplPGML(container)
{
this._curX = 0;
this._curY = 0;
this._root = null;
this._cntr = container;
...
}
VectorGraphicsImplPGML.prototype = {
LineTo: function(x, y, stroke_color)
{
if(!this._root) this._SetupRoot();
...
},
Rect: function(width, height, fill_color)
{
if(!this._root) this._SetupRoot();
...
},
...
}
VectorGraphics.prototype._EstablishImplementor:
_EstablishImplementor: function(container)
{
// Browsers/plugins that support SVG:
if((navigator.mimeTypes &&
(navigator.mimeTypes["image/svg+xml"] ...)))
return new VectorGraphicsImplSVG(container);
// Browsers/plugins that support PGML:
else if(navigator.mimeTypes &&
navigator.mimeTypes["image/pgml+xml"])
return new VectorGraphicsImplPGML(container);
else
{
...
}
...
}
I'd like to hear from you. Feel free to e-mail me if you have any questions and/or suggestions.
| You must Sign In to use this message board. | |||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 27 Jul 2007 Editor: Sean Ewington |
Copyright 2006 by Dmitry Khudorozhkov Everything else Copyright © CodeProject, 1999-2009 Web19 | Advertise on the Code Project |