Wicked Code
Silverlight Deep Zoom optimal nutzen
Jeff Prosise
Inhalt
Reparieren der Schwenklogik von Deep Zoom Composer
Zugreifen auf Unterbilder und Metadaten
Dynamisches Deep Zoom: Bereitstellen von Bildpixeln zur Laufzeit
DeepZoomTools.dll
Nach der Vorstellung von Silverlight Deep Zoom bei der MIX-Konferenz 2008 war die Begeisterung, die es auslöste, noch Wochen später spürbar. Deep Zoom, ein Nebenprodukt des Seadragon-Projekts von Microsoft Live Labs, ist eine Silverlight-Adaption einer Technologie zur effizienten Darstellung umfangreicher Bilddaten für Benutzer. Adaptionen für Windows Mobile und AJAX sind ebenfalls verfügbar und dienen zur Steigerung der Plattformreichweite.
Wenn Sie noch nie von Deep Zoom gehört haben, lassen Sie alles stehen und liegen, und besuchen Sie die anerkannte Deep Zoom-Website unter memorabilia.hardrock.com. Verwenden Sie den Mauszeiger zum Schwenken und das Mausrad zum Zoomen. Dank Deep Zoom müssen Sie nicht erst mehrere Gigabyte (oder Terabyte) an Bilddaten herunterladen, um in der umfangreichen Erinnerungsstücksammlung des Hard Rock Cafés zu stöbern. Deep Zoom lädt nur die benötigten Pixel mit der erforderlichen Auflösung herunter. In Silverlight verbirgt sich die Komplexität von Deep Zoom hinter einem bemerkenswerten Steuerelement namens MultiScaleImage. Nachdem eine Deep Zoom-Szene erstellt wurde (in der Regel mit Deep Zoom Composer, kostenlos herunterladbar unter go.microsoft.com/fwlink/?LinkId=148861), muss zum Anzeigen der Szene in einem Browser lediglich ein MultiScaleImage-Steuerelement deklariert werden und die Source-Eigenschaft des Steuerelements auf die Deep Zoom Composer-Ausgabe verweisen. Zur Unterstützung von interaktivem Schwenken und Zoomen ist nur ein wenig Code für das Mausverhalten erforderlich, der mit dem Steuerelement interagiert, doch selbst diesen stellt Deep Zoom Composer mittlerweile bereits zur Verfügung.
Trotz der Unkompliziertheit, mit der eine einfache Deep Zoom-Anwendung erstellt werden kann, entgeht Ihnen der tatsächliche Funktionsumfang von Deep Zoom, wenn Sie die Grenzen von Deep Zoom Composer akzeptieren. Wussten Sie beispielsweise, dass Sie mit einem Programm die Bilder in einer Deep Zoom-Szene ändern können, dass Sie Metadaten erstellen und den einzelnen Bildern zuordnen können, oder dass Deep Zoom-Bilder aus einer Datenbank stammen oder sich direkt zusammenstellen lassen? Einige der bemerkenswertesten Deep Zoom-Anwendungen basieren auf einer wenig bekannten Funktion von Deep Zoom, die die Plattform um eine völlig neue Dimension erweitert.
Wenn Sie Silverlight Deep Zoom optimal nutzen möchten, finden Sie im Folgenden drei Methoden, um genau dieses Ziel zu erreichen.
Reparieren der Schwenklogik von Deep Zoom Composer
Zunächst einmal gilt: Wenn Sie Deep Zoom optimieren möchten, sollten Sie vor allem wissen, dass Sie dem Code für das Mausverhalten von Deep Zoom Composer nicht trauen sollten. Der Code, der das Schwenken in der Szene als Reaktion auf MouseMove-Ereignisse bewirkt, verliert den Kontakt zur Maus, wenn Sie zu schnell schwenken. Probieren Sie es aus. Nehmen Sie eine beliebige, mit Deep Zoom Composer erstellte Deep Zoom-Anwendung, und platzieren Sie den Cursor auf einem erkennbaren Punkt oder Pixel in der Szene. Bewegen Sie den Mauszeiger dann einige Male schnell hin und her, hoch und runter. Beachten Sie, dass sich der Cursor nicht mehr am Ausgangspunkt befindet, wenn die Szene im Anschluss an die Cursorposition zurückspringt. Je länger und schneller die Bewegung, desto größer der Unterschied. Das ist zwar kein Dealbreaker, aber versuchen Sie dasselbe auf der Hard Rock Memorabilia-Site, und Sie werden feststellen, dass die Szene zuverlässig an die ursprüngliche Cursorposition zurückkehrt, ganz gleich, wie sehr Sie versuchen, sie auszutricksen.
Abbildung 1 zeigt, wie Sie den Code von Deep Zoom Composer ändern müssen, um das Problem zu beheben. Deklarieren Sie zunächst zwei neue Felder namens lastViewportOrigin und lastMousePosition in der Page-Klasse von Page.xaml.cs. (Wenn Sie schon dabei sind, können Sie auch gleich die Felder dragOffset und currentPosition löschen, da sie nicht benötigt werden.) Schreiben Sie dann die Handler MouseLeftButtonDown und MouseMove wie gezeigt um. Sie werden feststellen, dass die Szene genau an die ursprüngliche Cursorposition zurückkehrt, wenn Sie die Mausbewegung stoppen. Wenn Sie bei solchen Dingen ähnlich anspruchsvoll sind wie ich, werden Sie danach wieder gut schlafen können.
Abbildung 1: Reparieren der Schwenklogik von Deep Zoom Composer
Point lastViewportOrigin;
Point lastMousePosition;
...
this.MouseLeftButtonDown += delegate(object sender, MouseButtonEventArgs e)
{
mouseButtonPressed = true;
mouseIsDragging = false;
lastViewportOrigin = msi.ViewportOrigin;
lastMousePosition = e.GetPosition(msi);
};
this.MouseMove += delegate(object sender, MouseEventArgs e)
{
if (mouseIsDragging)
{
Point pos = e.GetPosition(msi);
Point origin = lastViewportOrigin;
origin.X += (lastMousePosition.X - pos.X) /
msi.ActualWidth * msi.ViewportWidth;
origin.Y += (lastMousePosition.Y - pos.Y) /
msi.ActualWidth * msi.ViewportWidth;
msi.ViewportOrigin = lastViewportOrigin = origin;
lastMousePosition = pos;
}
};
Zugreifen auf Unterbilder und Metadaten
Sie haben vielleicht bereits bemerkt, dass Sie beim Exportieren eines Projekts aus Deep Zoom Composer zwischen dem Export als Zusammenstellung oder als Sammlung wählen können. Die zweite Option hat einen sehr angenehmen Vorteil: Anstatt des Exports einer Deep Zoom-Szene, bei der alle hinzugefügten Bilder in einem einzigen Riesenbild zusammengefasst sind, wird eine Szene exportiert, die einzeln adressierbare Unterbilder enthält. Die Unterbilder werden durch die SubImages-Eigenschaft des MultiScaleImage-Steuerelements verfügbar gemacht. Da es sich um einzeln adressierbare Objekte handelt, können die Unterbilder modifiziert, animiert und desinfiziert (kleiner Scherz!) werden, um die Attraktivität und Interaktivität von Deep Zoom-Anwendungen zu erhöhen.
Jedes Element in der SubImages-Sammlung ist eine Instanz von MultiScaleSubImage, die von DependencyObject abgeleitet ist und die Eigenschaften AspectRatio, Opacity, ZIndex, ViewportOrigin und ViewportWidth umfasst. Die beiden letzten bestimmen in Kombination die Größe und Position eines Unterbilds in einer Deep Zoom-Szene. Denken Sie daran, dass die SubImages-Eigenschaft beim ersten Laden des MultiScaleImage-Steuerelements leer ist. Die erste Möglichkeit zum Durchlaufen der Unterbilder ist der Zeitpunkt, zu dem das Steuerelement das ImageOpenSucceeded-Ereignis auslöst.
Eine Verwendungsmöglichkeit für die SubImages-Eigenschaft sind Kollisionsabfragen (Hittests) einzelner Bilder zum Anzeigen der Metadaten – Titel, Beschreibungen usw. – als Reaktion auf Klicks oder Cursorbewegungen. Eine weitere Verwendungsmöglichkeit ist die Neuanordnung der Bilder in einer Deep Zoom-Szene mithilfe eines Programms. Die Anwendung DeepZoomTravelDemo in Abbildung 2 illustriert beide Möglichkeiten. Wenn Sie den Mauszeiger auf einem Bild der Szene platzieren, wird auf der rechten Seite ein halbtransparenter Informationsbereich mit einem Bildtitel und einer Beschreibung angezeigt. Wenn Sie nun oben links auf die Schaltfläche für zufällige Wiedergabe (Shuffle) klicken, ordnen die Bilder sich selbst in zufälliger Reihenfolge neu an.
Abbildung 2 DeepZoomTravelDemo
Die neun in DeepZoomTravelDemo enthaltenen Bilder sind Schnappschüsse von einigen meiner Auslandsreisen. Ich habe sie in Deep Zoom Composer importiert, anhand eines Rasters angeordnet und die Szene exportiert (als Sammlung). Dann habe ich die Ausgabe aus Deep Zoom Composer in ein Silverlight-Projekt importiert und eine ähnliche Zoom- und Schwenklogik wie im vorhergehenden Abschnitt hinzugefügt. Um eine angemessene Downloadgröße zu erzielen (13 MB anstelle von 170 MB), habe ich vor dem Hochladen der Anwendung in die MSDN Code Gallery die beiden untersten Ebenen der von Deep Zoom Composer erzeugten Bildpyramide gelöscht. Die Version, die Sie sich herunterladen können, lässt sich einwandfrei verwenden, beim Vergrößern wirken die Bilder jedoch viel schneller grobkörnig als bei der Originalversion.
Die Anzeige von Bilddaten als DeepZoomTravelDemo stellt den Entwickler vor zwei Herausforderungen. Erstens: Wo speichere ich die Metadaten, und wie ordne ich sie den Bildern in der Szene zu? Zweitens: Wie korreliere ich die Elemente in der SubImages-Sammlung des MultiScaleImage-Steuerelements mit Bildern in der Szene, da die MultiScaleSubImage-Klasse keine Informationen zu beiden bietet?
Die erste Aufgabe – Speichern der Metadaten – kann durch die Eingabe einer Textzeichenfolge in das Tag-Feld in der unteren rechten Ecke von Deep Zoom Composer, das angezeigt wird, wenn ein Bild ausgewählt ist, gelöst werden. Ich habe es zum Speichern des Titels und einer Beschreibung für jedes Bild, getrennt durch Pluszeichen, verwendet. Deep Zoom Composer schreibt die Tags in die Datei Metadata.xml, die beim Exportieren des Projekts erstellt wird. Jedes Bild in der Szene wird durch ein <Image>-Element in der Datei Metadata.xml dargestellt, und alle <Image>-Elemente enthalten ein Unterelement namens <Tag>, das das entsprechende Tag enthält. Abbildung 3 zeigt das <Image>-Element, das für das Bild in der oberen linken Ecke der Szene in die Datei Metadata.xml geschrieben wurde. Die Tag-Bearbeitung mit Deep Zoom Composer ist ein bisschen schwierig, da das Tag-Feld so klein ist. Sie können die Datei jedoch so wie ich manuell bearbeiten, um jedem Bild einen Titel und eine Beschreibung als Tag hinzuzufügen.
Abbildung 3: Ein <Image>-Element in der Datei Metadata.xml
<Image>
<FileName>
C:\Users\Jeff\Documents\Expression\Deep Zoom Composer
Projects\DeepZoomTravelDemo\source images\great wall of china.jpg
</FileName>
<x>0</x>
<y>0</y>
<Width>0.316957210776545</Width>
<Height>0.313807531380753</Height>
<ZOrder>1</ZOrder>
<Tag>
Great Wall of China+The Great Wall of China near Badaling, about an hour
north of Beijing. This portion of the Great Wall has been restored and
offers outstanding views of the surrounding mountains.
</Tag>
</Image>
Es wäre natürlich schön, wenn die MultiScaleSubImage-Klasse eine Tag-Eigenschaft hätte, die automatisch mit dem Inhalt des <Tag>-Elements initialisiert werden würde. Dem ist aber leider nicht so, d. h. Sie müssen improvisieren. Zunächst schreiben Sie ein wenig Code zum Herunterladen der Datei Metadata.xml und Parsen der darin enthaltenen Tags. Dann können Sie die <ZOrder>-Elemente in der Datei Metadata.xml dazu verwenden, <Image>-Elemente mit Bildern in der Deep Zoom-Szene zu korrelieren. Wenn die Szene neun Bilder enthält (und die SubImages-Sammlung des MultiScaleImage-Steuerelements daher neun MultiScaleSubImage-Objekte enthält), entspricht SubImages[0] dem Bild mit <ZOrder> = 1, SubImages[1] entspricht dem Bild mit <ZOrder> = 2 usw.
DeepZoomTravelDemo verwendet diese Korrelation zum Speichern von Bildtiteln und Beschreibungen. Zu Beginn verwendet der Page-Konstruktor ein WebClient-Objekt, um einen asynchronen Download der Datei Metadata.xml aus dem ClientBin-Ordner des Servers zu starten (siehe Abbildung 4). Nach Abschluss des Downloads erfolgt mithilfe der WebClient_OpenReadCompleted-Methode ein Parsen der heruntergeladenen XML-Datei mit XmlReader. Dann wird das Feld namens _Metadata mit einem Array von SubImageInfo-Objekten initialisiert, die Informationen über die Bilder in der Szene enthalten, einschließlich Titeln und Beschreibungen. Die Klasse sehen Sie hier:
public class SubImageInfo
{
public string Caption { get; set; }
public string Description { get; set; }
public int Index { get; set; }
}
Abbildung 4: Herunterladen der Datei Metadata.xml und Korrelieren von Metadaten und Unterbildern
private SubImageInfo[] _Metadata;
...
public Page()
{
InitializeComponent();
// Register mousewheel event handler
HtmlPage.Window.AttachEvent("DOMMouseScroll ", OnMouseWheelTurned);
HtmlPage.Window.AttachEvent( "onmousewheel ", OnMouseWheelTurned);
HtmlPage.Document.AttachEvent( "onmousewheel ", OnMouseWheelTurned);
// Fetch Metadata.xml from the server
WebClient wc = new WebClient();
wc.OpenReadCompleted += new
OpenReadCompletedEventHandler(WebClient_OpenReadCompleted);
wc.OpenReadAsync(new Uri( "Metadata.xml ", UriKind.Relative));
}
private void WebClient_OpenReadCompleted(object sender,
OpenReadCompletedEventArgs e)
{
if (e.Error != null)
{
MessageBox.Show( "Unable to load XML metadata ");
return;
}
// Create a collection of SubImageInfo objects from Metadata.xml
List<SubImageInfo> images = new List<SubImageInfo>();
try
{
XmlReader reader = XmlReader.Create(e.Result);
SubImageInfo info = null;
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element &&
reader.Name == "Image ")
info = new SubImageInfo();
else if (reader.NodeType == XmlNodeType.Element &&
reader.Name == "ZOrder ")
info.Index = reader.ReadElementContentAsInt();
else if (reader.NodeType == XmlNodeType.Element &&
reader.Name == "Tag ")
{
string[] substrings =
reader.ReadElementContentAsString().Split('+');
info.Caption = substrings[0];
if (substrings.Length > 1)
info.Description = substrings[1];
else
info.Description = String.Empty;
}
else if (reader.NodeType == XmlNodeType.EndElement &&
reader.Name == "Image ")
images.Add(info);
}
}
catch (XmlException)
{
MessageBox.Show( "Error parsing XML metadata ");
}
// Populate the _Metadata array with ordered data
_Metadata = new SubImageInfo[images.Count];
foreach (SubImageInfo image in images)
_Metadata[image.Index - 1] = image;
}
Die aus der Datei Metadata.xml gelesenen <ZOrder>-Werte dienen zum Sortieren der SubImageInfo-Objekte im _Metadata-Array, um sicherzustellen, dass die Reihenfolge der Elemente im _Metadata-Array mit der Reihenfolge der Elemente in der SubImages-Sammlung von MultiScaleImage übereinstimmt. Mit anderen Worten: _Metadata[0] enthält Titel und Beschreibung für SubImages[0], _Metadata[1] enthält Titel und Beschreibung für SubImages[1] usw. Übrigens habe ich anstelle von LINQ to XML XmlReader verwendet, um zu verhindern, dass die XAP-Datei durch Hinzufügen einer zusätzlichen, von LINQ to XML benötigten Assembly (System.Xml.Linq.dll) zu groß wird.
Nachdem _Metadata mit SubImageInfo-Objekten, die Titel und Beschreibungen enthalten, initialisiert wurde, müssen Sie als nächstes den Code zum Anzeigen der Titel und Beschreibungen schreiben. Diesen Vorgang zeigt Abbildung 5. Der MouseMove-Handler zum Schwenken der Deep Zoom-Szene bei gedrückter linker Maustaste verhält sich anders, wenn die linke Maustaste nicht gedrückt ist: Es wird eine Kollisionsabfrage der Szene durchgeführt, um festzustellen, ob sich der Cursor gerade über einem der Unterbilder befindet. Kollisionsabfragen erfolgen mit der Hilfsmethode GetSubImageIndex, die den Wert -1 zurückgibt, wenn sich der Cursor nicht über einem Unterbild befindet. Ist der Cursor über einem Unterbild platziert, wird ein 0-basierter Bildindex zurückgegeben. Dieser Index gibt sowohl ein Unterbild in MutliScaleImage.SubImages und ein SubImageInfo-Objekt in _Metadata an. Mit einigen Codezeilen können der Titel und die Beschreibung aus dem SubImageInfo-Objekt in ein TextBlock-Paar kopiert werden. Eine weitere Codezeile löst eine Animation aus, durch die der Informationsbereich angezeigt wird, wenn dies nicht bereits der Fall ist. Beachten Sie, dass GetSubImageIndex die Unterbilder in umgekehrter Reihenfolge auf Kollisionen prüft, da das letzte Unterbild in der SubImages-Sammlung des MultiScaleImage-Steuerelements in der Z-Reihenfolge an erster Stelle steht. Das vorletzte Unterbild steht in der Z-Reihenfolge an zweiter Stelle usw.
Abbildung 5: Kollisionsabfrage der Unterbilder
private int _LastIndex = -1;
...
private void MSI_MouseMove(object sender, MouseEventArgs e)
{
if (_Dragging)
{
// If the left mouse button is down, pan the Deep Zoom scene
...
}
else
{
// If the left mouse button isn't down, update the infobar
if (_Metadata != null)
{
int index = GetSubImageIndex(e.GetPosition(MSI));
if (index != _LastIndex)
{
_LastIndex = index;
if (index != -1)
{
Caption.Text = _Metadata[index].Caption;
Description.Text = _Metadata[index].Description;
FadeIn.Begin();
}
else
{
FadeOut.Begin();
}
}
}
}
}
private int GetSubImageIndex(Point point)
{
// Hit-test each sub-image in the MultiScaleImage control to determine
// whether "point " lies within a sub-image
for (int i = MSI.SubImages.Count - 1; i >= 0; i--)
{
MultiScaleSubImage image = MSI.SubImages[i];
double width = MSI.ActualWidth /
(MSI.ViewportWidth * image.ViewportWidth);
double height = MSI.ActualWidth /
(MSI.ViewportWidth * image.ViewportWidth * image.AspectRatio);
Point pos = MSI.LogicalToElementPoint(new Point(
-image.ViewportOrigin.X / image.ViewportWidth,
-image.ViewportOrigin.Y / image.ViewportWidth)
);
Rect rect = new Rect(pos.X, pos.Y, width, height);
if (rect.Contains(point))
{
// Return the image index
return i;
}
}
// No corresponding sub-image
return -1;
}
Neben der Unterstützung für Cursorbewegungen können Sie mit DeepZoomTravelDemo die Bilder in der Szene neu anordnen. Wenn Sie es noch nicht getan haben, klicken Sie einfach mal auf die Schaltfläche für zufällige Wiedergabe in der oberen linken Ecke der Szene. (Klicken Sie am besten mehrmals darauf; die Bilder nehmen jedes Mal eine andere Reihenfolge an.) Die Neuanordnung erfolgt mithilfe der Shuffle-Methode (siehe Abbildung 6). Es wird ein Array erstellt, das die ViewportOrigins aller Bilder enthält. Anschließend wird das Array mithilfe eines Zufallszahlengenerators neu angeordnet und ein Storyboard und eine Reihe von PointAnimations erstellt, um die Unterbilder an die im neu angeordneten Array enthaltenen Positionen zu verschieben. Der Schlüssel liegt darin, dass die Unterbilder durch die SubImages-Eigenschaft des MultiScaleImage-Steuerelements Ihrem Code verfügbar gemacht werden, und dass Sie die ViewportOrigin-Eigenschaft jedes Unterbilds so bearbeiten können, dass die Position in der Szene geändert wird.
Abbildung 6: Zufällige Wiedergabe der Unterbilder
private void Shuffle()
{
// Create a randomly ordered list of sub-image viewport origins
List<Point> origins = new List<Point>();
foreach (MultiScaleSubImage image in MSI.SubImages)
origins.Add(image.ViewportOrigin);
Random rand = new Random();
int count = origins.Count;
for (int i = 0; i < count; i++)
{
Point origin = origins[i];
origins.RemoveAt(i);
origins.Insert(rand.Next(count), origin);
}
// Create a Storyboard and animations for shuffling
Storyboard sb = new Storyboard();
for (int i = 0; i < count; i++)
{
PointAnimation animation = new PointAnimation();
animation.Duration = TimeSpan.FromMilliseconds(250);
animation.To = origins[i];
Storyboard.SetTarget(animation, MSI.SubImages[i]);
Storyboard.SetTargetProperty(animation,
new PropertyPath( "ViewportOrigin "));
sb.Children.Add(animation);
}
// Run the animations
sb.Begin();
}
Bei der Recherche für diese Kolumne habe ich zahlreiche Blogeinträge mit nützlichen Informationen gefunden. Dazu zählen u. a. Working with Collections in Deep Zoom (Arbeiten mit Sammlungen in Deep Zoom) von Jaime Rodriquez und Deep Zoom Composer—Filtering by Tag Sample (Deep Zoom Composer – Filtern nach Tag-Mustern), der von einem Mitglied des Expression Blend-Teams verfasst wurde und eine Technik zum Filtern von Deep Zoom-Bildern auf Grundlage von Bild-Tags beschreibt. Die Implementierung von Cursorbewegungen, die Neuanordnung der Bilder in einer Szene und das Filtern von Bildern anhand von Tag-Daten sind nur einige der Funktionen, die durch die Möglichkeit zum Adressieren der einzelnen Unterbilder in einer Deep Zoom-Szene und zum Verknüpfen mit Metadaten bereitgestellt werden.
Optimieren von dynamischem Deep Zoom
Fraktale faszinieren mich, seit ich vor über 20 Jahren auf sie aufmerksam geworden bin. Wenn ich mich recht erinnere, schrieb ich meinen ersten Mandelbrot-Viewer in den frühen 1990er Jahren. In meinem Bücherregal mit alten, aber informativen Computerbüchern findet sich immer noch eine makellose Ausgabe von „Fractal Image Compression“ von Barnsley und Hurd, das ich für ein Forschungsprojekt zum Thema Datenkomprimierung Mitte der 1990er Jahre verwendet habe. Und eins meiner absoluten Lieblingsbücher aller Zeiten ist „Chaos“ von James Gleick (Penguin, 2008).
Seitdem ich Silverlight zum ersten Mal sah, wollte ich damit einen interaktiven und visuell ansprechenden Mandelbrot-Viewer für Browser entwickeln. Durch dynamisches Deep Zoom wurde dies möglich. Der Nachteil ist allerdings, dass die Bilder auf dem Server generiert und auf den Client heruntergeladen werden. Dies führt zu unerwünschter Latenz und steigert natürlich die Arbeitslast des Servers.
Silverlight 2 beinhaltet keine API zum Erzeugen von Bitmaps auf dem Client, Sie können sie jedoch mit dem Silverlight PNG-Encoder von Joe Stegman trotzdem generieren. Minh Nguyen hat damit Mandelbrot Explorer entwickelt. Alle Informationen hierzu finden Sie in seinem Blogbeitrag Minh T. Nguyen's Mandelbrot Explorer 1.0 in Silverlight 2.0 with source code (Mandelbrot Explorer 1.0 in Silverlight 2.0 mit Quellcode von Minh T. Nguyen). Silverlight 3, das sich in der Beta-Phase befinden wird, wenn Sie diese Zeilen lesen, verfügt über eine Bitmap-API, es bleibt jedoch weiterhin ein Problem, dass Deep Zoom Bilder vom Server abrufen will. Zurzeit ist noch unklar, ob die nächste Deep Zoom-Version eine Implementierung auf Client-Seite enthalten wird. Falls ja, können Sie jedenfalls darauf wetten, dass ich MandelbrotDemo für Silverlight 3 vollständig auf Client-basierte Funktionalität umstellen werde.
Dynamisches Deep Zoom: Bereitstellen von Bildpixeln zur Laufzeit
Mit der Export-Funktion von Deep Zoom Composer können alle Daten erzeugt werden, die ein MultiScaleImage-Steuerelement benötigt. Dazu zählt auch eine XML-Datei (dzc_output.xml), die auf andere XML-Dateien verweist, die wiederum auf die einzelnen Bilder in der Szene verweisen. Die Deep Zoom Composer-Ausgabe umfasst außerdem Hunderte (manchmal Tausende), aus diesen Bildern erzeugte Bildkacheln. Die Kacheln ergeben eine Bildpyramide, wobei jede Ebene der Pyramide eine gekachelte Version des Originalbilds enthält und eine andere Auflösung darstellt. Die oberste Ebene der Pyramide enthält beispielsweise eine einzige Kachel mit einer 256x256 Pixel großen Version des Bildes. Die nächste Ebene enthält dann vier 256x256 Pixel große Kacheln, die zusammengesetzt eine 512x512 Pixel große Version des Bildes ergeben. Die wiederum nächste Ebene enthält sechzehn 256x256-Kacheln, die unterschiedliche Teile eines 1.024x1.024 Pixel großen Bildes ergeben usw. Deep Zoom Composer erzeugt so viele Ebenen wie erforderlich, um das Originalbild in seiner ursprünglichen Auflösung darzustellen. Wenn ein Benutzer die Zoom- und Schwenkfunktionen in einer Deep Zoom-Szene verwendet, sendet das MultiScaleImage-Steuerelement kontinuierlich HTTP-Anforderungen zum Abrufen von Bildkacheln mit der richtigen Auflösung an den Server. Außerdem erfolgt eine geschickte Überblendung, um die Übergänge zwischen den einzelnen Ebenen zu kaschieren.
Was Sie über das MultiScaleImage-Steuerelement wahrscheinlich nicht wissen: Es benötigt Deep Zoom Composer gar nicht. Deep Zoom Composer ist eigentlich nur ein Werkzeug zum schnellen und einfachen Erstellen von Deep Zoom-Objekten, die aus statischen Bildern erstellte Szenen enthalten. Als Alternative zur Bereitstellung von MultiScaleImage mit statischem Inhalt können Sie als Reaktion auf Anforderungen von MultiScaleImage zur Laufzeit Inhalte erzeugen und diese auf den Client herunterladen.
Warum sollten Sie Deep Zoom-Inhalte zur Laufzeit erzeugen wollen? Ich werde von Entwicklern ständig gefragt, wie das funktioniert. „Ist es möglich, Deep Zoom dynamisch Bilddaten zur Verfügung zu stellen?“ Der Grund ist, dass dies Raum für eine ganz neue Klasse von Deep Zoom-Anwendungen schafft, die Bildkacheln aus Datenbanken abrufen und im Handumdrehen Bildkacheln erzeugen.
Beispiel gefällig? Schauen Sie sich die Website des Deep Earth-Projekts sowie ein Beispiel für Deep Earth in Aktion unter deepearth.soulsolutions.com an. Deep Earth wird als Karten-Steuerelement auf Grundlage der Kombination der Microsoft Silverlight 2-Plattform und des DeepZoom (MultiScaleImage)-Steuerelements bezeichnet. Mit anderen Worten: es handelt sich um ein Steuerelement, das Sie in eine Silverlight-Anwendung einfügen können, um die riesige Menge geografischer Daten aus Microsoft Virtual Earth über ein Deep Zoom-Front-End darstellbar zu machen. Sie können im Weltall starten und dann bis in Ihren Vorgarten zoomen. Dank der Funktionsweise von MultiScaleImage und der Deep Zoom-Laufzeit hinter den Kulissen erfolgt das Zoomen erstaunlich problemlos.
Deep Earth basiert nicht auf XML-Dateien und von Deep Zoom Composer ausgegebenen Bildkacheln, es stellt dem MultiScaleImage-Steuerelement dynamisch Bildkacheln bereit, und es ruft die Bildkacheln aus Virtual Earth ab. Deep Earth-Benutzer bezeichnen dies als „dynamisches Deep Zoom“.
Die Anwendung in Abbildung 7 illustriert die Grundlagen von dynamischem Deep Zoom. MandelbrotDemo bietet einen Deep Zoom-basierten Einblick in die Mandelbrot-Menge, das möglicherweise bekannteste Fraktal der Welt. Die Mandelbrot-Menge ist unendlich komplex, d. h. man könnte unendlich lange hineinzoomen, und die Detailliertheit würde nie nachlassen. In der Softwarewelt finden sich zahlreiche Mandelbrot-Viewer, doch nur wenige sind so raffiniert wie der, den Deep Zoom verwendet. Versuchen Sie es selbst: Führen Sie MandelbrotDemo aus, und zoomen Sie in einige der Bereiche am Rand der Mandelbrot-Menge (an der Grenze zwischen Schwarz und hellen Farben). Sie können nicht unendlich hineinzoomen, da selbst eine dynamische Deep Zoom-Szene eine endliche Breite und Höhe hat, aber die Abmessungen der Szene können sehr, sehr groß sein (bis zu 232 Pixel pro Seite).
Zwei Ansichten der Mandelbrot-Menge
Der erste Schritt bei der Implementierung von dynamischem Deep Zoom ist, eine Ableitung von der Silverlight-Klasse MultiScaleTileSource durchzuführen, die sich im System.Windows.Media-Namespace von System.Windows.dll befindet, und die GetTileLayers-Methode außer Kraft zu setzen. Jedes Mal, wenn das MultiScaleImage-Steuerelement eine Kachel benötigt, wird GetTileLayers aufgerufen. Ihre Aufgabe ist es, eine Bildkachel zu erstellen und an das MultiScaleImage-Steuerelement zurückzugeben, indem Sie sie zur IList hinzufügen, die in der Parameterliste von GetTileLayers übergeben wird. Andere Parametereingaben in GetTileLayers legen den Zoomfaktor (eigentlich die Ebene der Bildpyramide, aus der Kacheln angefordert werden) und die X- und Y-Position der angeforderten Kachel innerhalb dieser Ebene der Pyramide fest. Genau wie die Werte x, y und z zur Identifizierung eines Punkts in einem dreidimensionalen Koordinatensystem ausreichen, identifizieren ein X-Wert, ein Y-Wert und eine Ebene eindeutig eine Bildkachel in einer Deep Zoom-Bildpyramide.
Abbildung 8 zeigt die in MandelbrotDemo verwendete, von MultiScaleTileSource-abgeleitete Klasse. Durch das Außerkraftsetzen von GetTileLayers wird eigentlich nur eine HTTP-Anforderung für die Bildkachel an den Server gesendet. Der Endpunkt für diese Anforderung ist ein HTTP-Handler namens MandelbrotImageGenerator.ashx. Bevor wir den Handler näher untersuchen, schauen wir uns aber erstmal an, wie MandelbrotTileSource mit einem MultiScaleImage-Steuerelement verbunden ist.
Abbildung 8: MultiScaleTileSource-Ableitung
public class MandelbrotTileSource : MultiScaleTileSource
{
private int _width; // Tile width
private int _height; // Tile height
public MandelbrotTileSource(int imageWidth, int imageHeight,
int tileWidth, int tileHeight) :
base(imageWidth, imageHeight, tileWidth, tileHeight, 0)
{
_width = tileWidth;
_height = tileHeight;
}
protected override void GetTileLayers(int level, int posx, int posy,
IList<object> sources)
{
string source = string.Format(
"http://localhost:50216/MandelbrotImageGenerator.ashx? " +
"level={0}&x={1}&y={2}&width={3}&height={4} ",
level, posx, posy, _width, _height);
sources.Add(new Uri(source, UriKind.Absolute));
}
}
Abbildung 9 zeigt einen Auszug aus der MandelbrotDemo-Datei Page.xaml.cs, d. h. den XAML-Code, der hinter dem Klassen-Konstruktor steckt. Die Hauptanweisung erzeugt ein MandelbrotTileSource-Objekt und weist der Source-Eigenschaft von MultiScaleImage einen Verweis darauf zu. Für statisches Deep Zoom legen Sie für Source die URI von dzc_output.xml fest. Für dynamisches Deep Zoom verweisen Sie stattdessen auf ein MultiScaleTileSource-Objekt. Das so erstellte MandelbrotTileSource-Objekt gibt an, dass jede Seite des bereitgestellten Bildes 230 Pixel groß ist und das Bild in 128x128 Pixel große Kacheln aufgeteilt wird.
Abbildung 9: Registrieren eines Deep Zoom-TileSource-Objekts
public Page()
{
InitializeComponent();
// Point MultiScaleImage control to dynamic tile source
MSI.Source = new MandelbrotTileSource((int)Math.Pow(2, 30),
(int)Math.Pow(2, 30), 128, 128);
// Register mousewheel event handler
HtmlPage.Window.AttachEvent( "DOMMouseScroll ", OnMouseWheelTurned);
HtmlPage.Window.AttachEvent( "onmousewheel ", OnMouseWheelTurned);
HtmlPage.Document.AttachEvent( "onmousewheel ", OnMouseWheelTurned);
}
Die Bildkacheln werden von MandelbrotImageGenerator.ashx auf dem Server erzeugt (siehe Abbildung 10). Nach dem Abrufen von Eingabeparametern aus der Abfragezeichenfolge wird eine Bitmap-Datei erstellt, die die anforderte Kachel darstellt. Dann werden die Bildbits in die HTTP-Antwort geschrieben. DrawMandelbrotTile übernimmt die Pixel-Erzeugung. Nach dem Aufrufen von DrawMandelbrotTile wird der X-Y-Ebenen-Wert, der die angeforderte Kachel anhand von Koordinaten in der komplexen Ebene (eine mathematische Ebene, in der reelle Zahlen entlang der x-Achse und imaginäre Zahlen, d. h. Zahlen, die die Quadratwurzel von -1 beinhalten, entlang der y-Achse dargestellt werden) identifiziert, umgewandelt. Dann erfolgt die Iteration ... durch alle Punkte in der komplexen Ebene, die Pixeln in der Bildkachel entsprechen. Dabei wird jeder Punkt geprüft, um festzustellen, ob er zur Mandelbrot-Menge gehört. Dem zugehörigen Pixel wird eine Farbe zugewiesen, die seine Beziehung zur Mandelbrot-Menge repräsentiert (mehr dazu im nächsten Abschnitt).
Abbildung 10: HTTP-Handler zum Erzeugen von Deep Zoom-Bildkacheln
public class MandelbrotImageGenerator : IHttpHandler
{
private const int _max = 128; // Maximum number of iterations
private const double _escape = 4; // Escape value squared
public void ProcessRequest(HttpContext context)
{
// Grab input parameters
int level = Int32.Parse(context.Request[ "level "]);
int x = Int32.Parse(context.Request[ "x "]);
int y = Int32.Parse(context.Request[ "y "]);
int width = Int32.Parse(context.Request[ "width "]);
int height = Int32.Parse(context.Request[ "height "]);
// Generate the bitmap
Bitmap bitmap = DrawMandelbrotTile(level, x, y, width, height);
// Set the response's content type to image/jpeg
context.Response.ContentType = "image/jpeg ";
// Write the image to the HTTP response
bitmap.Save(context.Response.OutputStream, ImageFormat.Jpeg);
// Clean up and return
bitmap.Dispose ();
}
public bool IsReusable
{
get { return true; }
}
private Bitmap DrawMandelbrotTile(int level, int posx, int posy,
int width, int height)
{
// Create a bitmap to represent the requested tile
Bitmap tile = new Bitmap(width, height);
// Compute the number of tiles in each direction at this level
int cx = Math.Max(1, (int)Math.Pow(2, level) / width);
int cy = Math.Max(1, (int)Math.Pow(2, level) / height);
// Compute starting values for real and imaginary components
// (from -2.0 - 1.5i to 1.0 + 1.5i)
double r0 = -2.0 + (3.0 * posx / cx);
double i0 = -1.5 + (3.0 * posy / cy);
// Compute increments for real and imaginary components
double dr = (3.0 / cx) / (width - 1);
double di = (3.0 / cy) / (height - 1);
// Iterate by row and column checking each pixel for
// inclusion in the Mandelbrot set
for (int x = 0; x < width; x++)
{
double cr = r0 + (x * dr);
for (int y = 0; y < height; y++)
{
double ci = i0 + (y * di);
double zr = cr;
double zi = ci;
int count = 0;
while (count < _max)
{
double zr2 = zr * zr;
double zi2 = zi * zi;
if (zr2 + zi2 > _escape)
{
tile.SetPixel(x, y,
ColorMapper.GetColor(count, _max));
break;
}
zi = ci + (2.0 * zr * zi);
zr = cr + zr2 - zi2;
count++;
}
if (count == _max)
tile.SetPixel(x, y, Color.Black);
}
}
// Return the bitmap
return tile;
}
}
Leider gibt es nahezu keine Dokumentation zur MultiScaleTileSource-Klasse von Silverlight. Damit Sie nicht denken, ich wäre ein Genie, weil ich mir das alles hier ausgedacht habe (jeder, der mich kennt, wird bezeugen, dass dem nicht so ist), möchte ich meine Informationsquellen nicht im Verborgenen halten: Ehre, wem Ehre gebührt. Während ich mit der Bedeutung der Eingabeparameter und der Zuordnung von Deep Zoom-X-Y-Ebenen-Werten zur komplexen Ebene zu kämpfen hatte, fand ich einen exzellenten Blogbeitrag von Mike Ormond, Deep Zoom, MultiScaleTileSource and the Mandelbrot Set (Deep Zoom, MultiScaleTileSource und die Mandelbrot-Menge). Dieser Eintrag offenbarte mir wichtige Einblicke in dynamisches Deep Zoom und enthielt einen Verweis auf einen anderen Blogbeitrag, The Mandelbrot Set (Die Mandelbrot-Menge), in dem ein effizienter Ansatz zur Berechnung der Mandelbrot-Menge beschrieben wird. Dank der bereits geleisteten Arbeit dieser Autoren wurde mein Arbeitsaufwand nahezu halbiert.
Noch eine letzte Bemerkung zu meiner Implementierung: Nahezu jede Anwendung zur Wiedergabe der Mandelbrot-Menge verwendet ein anderes Farbschema. Ich habe ein Schema verwendet, das Pixeln, die Koordinaten darstellen, die zur Mandelbrot-Menge gehören, die Farbe Schwarz, und Pixeln, die Koordinaten außerhalb der Mandelbrot-Menge darstellen, RGB-Farben zuweist. Je größer die Entfernung einer Koordinate von der Mandelbrot-Menge, desto „kälter“ (oder blauer) die Farbe. Je geringer die Entfernung zur Mandelbrot-Menge, desto „wärmer“ die Farbe. Die Entfernung von der Mandelbrot-Menge wird dadurch ermittelt, wie schnell der Punkt die Unendlichkeit erreicht. Im vorliegenden Code handelt es sich um die Anzahl der Iterationen, die die while-Schleife von DrawMandelbrotTile benötigt, um festzustellen, dass der Punkt kein Bestandteil der Mandelbrot-Menge ist. Je weniger Iterationen, desto weiter liegt der Punkt von der Punktmenge entfernt, die die Mandelbrot-Menge ausmacht. Ich habe den Code, der auf Grundlage der Iterationsanzahl einen RGB-Farbwert erzeugt, in einer eigenen Klasse namens ColorMapper erfasst (Abbildung 11). Wenn Sie mit unterschiedlichen Farbschemata experimentieren möchten, ändern Sie einfach die GetColor-Methode. Gehen Sie wie folgt vor, um das Ergebnis in Graustufen anzuzeigen:
int val = (count * 255) / max;
return Color.FromArgb(val, val, val);
Abbildung 11: ColorMapper-Klasse
public class ColorMapper
{
public static Color GetColor(int count, int max)
{
int h = max >> 1; // Divide max by 2
int q = max >> 2; // Divide max by 4
int r = (count * 255) / max;
int g = ((count % h) * 255) / h;
int b = ((count % q) * 255) / q;
return Color.FromArgb(r, g, b);
}
}
DeepZoomTools.dll
Eine letzte Information bezüglich Deep Zoom, die Sie vielleicht hilfreich finden werden, dreht sich um eine Assembly namens DeepZoomTools.dll. Deep Zoom Composer verwendet diese Assembly zum Erzeugen gekachelter Bilder und Metadaten aus den von Ihnen erstellten Szenen. Theoretisch könnten Sie damit selbst Zusammensetzungstools erstellen. Ich schreibe absichtlich „theoretisch“, denn es gibt herzlich wenig Dokumentation zu diesem Thema. Weitere Informationen zu DeepZoomTools.dll finden Sie im Blog von Expression Blend and Design. Und schicken Sie mir eine E-Mail, wenn Ihnen ein einzigartiger, kreativer Verwendungszweck für Deep Zoom einfällt, Sie aber nicht sicher sind, wie Sie die gewünschte Funktionalität realisieren können.
Senden Sie Fragen und Kommentare für Jeff an wicked@microsoft.com.
Jeff Prosise arbeitet als Autor für MSDN Magazine und ist der Verfasser mehrerer Bücher, einschließlich Programming Microsoft .NET (Microsoft Press, 2002). Er ist außerdem Mitbegründer von Wintellect (www.wintellect.com), einem Unternehmen für Softwareberatung und -schulung, das sich auf Microsoft .NET spezialisiert hat. Haben Sie einen Kommentar zu dieser Kolumne? Sie erreichen Jeff unter wicked@microsoft.com.