Flickr Atom Viewer

 

スクリプト

FlickrAtomViewer.fx

package net.javainthebox.flickrviewer;
  
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
 
var imagePane = ImagePane {
    translateX: 120
}
 
var thumbnailPane = ThumbnailPane {
    update: imagePane.update
}
 
var reader = AtomReadWorker {
    url: "http://api.flickr.com/services/feeds/photos_public.gne?id=57085156@N00&lang=en-us&format=atom"
    thumbnailPane: thumbnailPane
}
reader.execute();
 
Stage {
    title: "Flickr Searcher"
    scene: Scene {
        width: 660
        height: 660
        fill: Color.BLACK
 
        content: [
            imagePane,
            thumbnailPane
        ]
    }
}

Thumbnail.fx

package net.javainthebox.flickrviewer;
 
import javafx.scene.CustomNode;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.effect.DropShadow;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
 
public class Thumbnail extends CustomNode {
    // 選択された時にコールされるコールバック関数
    public var action: function(thumbnail: Thumbnail): Void;
 
    // イメージのタイトル
    public var title: String;

    // サムネイルイメージの URL
    // URLが変更したら、イメージをロードする
    public var thumbnailUrl: String on replace {
        if (thumbnailUrl != null) {
            thumbnail = Image {
                url: thumbnailUrl
            }
        }
    }
         
    // サムネイルイメージ
    var thumbnail: Image;
 
    // イメージのURL
    public var imageUrl: String;
 
    public-read def width: Number = 110.0;
    public-read def height: Number = 110.0;
 
    // 選択状態を表すフラグ
    public var selected: Boolean = false;
    // クリックされたら選択状態とし、コールバック関数をコールする
    override var pressed on replace {
        if (not selected and pressed) {
            selected = true;
            action(this);
        }
    }
 
    public override function create(): Node {
        Group {
            content: [
                Rectangle {
                    // 角丸四角にする
                    arcHeight: 10
                    arcWidth: 10
                    x: 0
                    y: 0
                    width: 109
                    height: 109
                    // 選択状態ではグレー、非選択状態では黒
                    fill: bind if (selected) Color.web("#666666") else  Color.web("#222222")
                    stroke: Color.web("#666666")
                    // ドロップシャドウを施すことにより浮きでるような効果を出す
                    effect: DropShadow {
                        offsetX: 1
                        offsetY: 1
                        radius: 1
                        color: Color.LIGHTGRAY
                    }
                },
                ImageView {
                    // イメージは中央に配置する
                    x: bind (width - thumbnail.width)/2
                    y: bind (height - thumbnail.height)/2
                    image: bind thumbnail
                }
            ]
        }
    }
}

ThumbnailPane.fx

package net.javainthebox.flickrviewer;
 
import javafx.scene.CustomNode;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.shape.Polygon;
import javafx.scene.shape.Rectangle;
 
// 三角形 サムネイルのページ送りに使用する
class Triangle extends CustomNode {
    var x: Integer;
    var y: Integer;
    var width: Integer;
    var height: Integer;
    var isUpward: Boolean;
    var action: function(): Void;
 
    override var onMouseClicked = function(event: MouseEvent) {
        if (not disable) {
            action();
        }
    }
 
    override function create(): Node {
        Group {
            content: [
                Rectangle {
                    x: x
                    y: y
                    width: width,
                    height: height
                    stroke: Color.web("#666666")
                    fill: bind if (not disable) {
                        if (pressed) {
                            Color.GRAY
                        } else if (hover) {
                            Color.web("#666666")
                        } else {
                            Color.BLACK;
                        }
                    } else {
                        Color.BLACK;
                    }
                },
                if (isUpward) {
                    Polygon {
                        points: [
                            x + width/2, y + height*.2,
                            x + width/2 + height*.3, y + height*.8,
                            x + width/2 - height*.3, y + height*.8
                        ]
                        fill: bind if (disable) Color.GRAY else Color.WHITE
                    }
                } else {
                    Polygon {
                        points: [
                            x + width/2, y + height*.8,
                            x + width/2 + height*.3, y + height*.2,
                            x + width/2 - height*.3, y + height*.2
                        ]
                        fill: bind if (disable) Color.GRAY else Color.WHITE
                    }
                }
            ]
        }
    }
}
 
// サムネイルを表示するペイン
public class ThumbnailPane extends CustomNode {
    // サムネイルの選択状態が変更したらコールするコールバック関数
    public-init var update: function(title: String, url: String);
 
    // サムネイル
    // サムネイルの個数に応じて、ページ送りのフラグをセットする
    public var thumbnails: Thumbnail[] on replace {
        if (calcIndex(page) < sizeof thumbnails) {
            forwardRemainsFlag = true;
        }
        for (thumbnail in thumbnails) {
            thumbnail.action = thumbnailSelected;
        }
        updateVisibleThumbnails();
    }
 
    // 現在表示しているサムネイル
    var visibleThumbnails: Thumbnail[];
    // 選択されているサムネイル
    var selectedThumbnail: Thumbnail;
    // サムネイルのページ数
    var page: Integer = 0;
    // ページ送りができるかどうかを示すフラグ
    var forwardRemainsFlag = false;
    var backwardRemainsFlag = false;
 
    def numPerVertical = 5;
    def thumbnailSize = 120;
    public-read def width: Integer = 120;
    public-read def height: Integer = 660;
    def arrowHeight = 25;
 
    // サムネイルの選択状態が変更したらコールされる関数
    function thumbnailSelected(thumbnail: Thumbnail): Void {
        if (selectedThumbnail != null) {
            selectedThumbnail.selected = false;
        }
        selectedThumbnail = thumbnail;
        update(selectedThumbnail.title, selectedThumbnail.imageUrl);
    }
 
    // ページ送りされた場合に表示を更新する関数
    function updateVisibleThumbnails() {
        visibleThumbnails = for (y in [0.. numPerVertical - 1]) {
            var index = page * numPerVertical + y;
            var thumbnail = thumbnails[index];
            thumbnail.translateX = 5;
            thumbnail.translateY = y * thumbnailSize + 35;
            thumbnail;
        }
    }

    function calcIndex(page: Integer): Integer {
        (page + 1) * numPerVertical;
    }

    public override function create(): Node {
        Group{
            content: bind [
                Rectangle {
                    x: 0 y:0
                    width: width height: height
                    fill: null
                    stroke: Color.web("#666666")
                }
                Triangle {
                    x: 0 y: 0
                    width: width height: arrowHeight
                    isUpward: true
                    disable: bind not backwardRemainsFlag
                    action: function(): Void {
                        page--;
                        if (page == 0) {
                            backwardRemainsFlag = false;
                        }
                        if (forwardRemainsFlag == false
                            and calcIndex(page) < sizeof thumbnails) {
                            forwardRemainsFlag = true;
                        }
                        updateVisibleThumbnails();
                    }
                }
                visibleThumbnails,
                Triangle {
                    x: 0 y: height - arrowHeight
                    width: width height: arrowHeight
                    isUpward: false
                    disable: bind not forwardRemainsFlag
                    action: function(): Void {
                        page++;
                        if (calcIndex(page) >= sizeof thumbnails) {
                            forwardRemainsFlag = false;
                        }
                        if (backwardRemainsFlag == false) {
                            backwardRemainsFlag = true;
                        }
                        updateVisibleThumbnails();
                    }
                }
            ]
        }
    }
}

ImagePane.fx

package net.javainthebox.flickrviewer;
 
import javafx.scene.CustomNode;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.effect.Reflection;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.Text;

// イメージを表示するペイン
public class ImagePane extends CustomNode {
    public-read var width = 540;
    public-read var height = 660;
 
    // 選択されたサムネイルが変更された時にコールされる関数
    public function update(title: String, url: String): Void {
        // イメージ
        var image = Image {
            url: url
            // バックグラウンドでイメージをロードする
            backgroundLoading: true
        }
        imageView = ImageView {
            translateX: bind (width - image.width) / 2
            translateY: bind (height - image.height) / 2
 
            // 下にイメージを反射させるエフェクト
            effect: Reflection {
                fraction: 0.4
            }
            image: image
        }
 
        // イメージのタイトル
        label = Text {
            translateX: 10
            translateY: 30

            font: Font {
                size: 18
            }
            fill: Color.WHITE
            content: title
        }
    }
 
    var imageView: ImageView;
    var label: Text;
 
    public override function create(): Node {
        Group {
            content: bind [label, imageView]
        }
    }
}

AtomReadWorker.fx

package net.javainthebox.flickrviewer;
 
import javafx.data.pull.Event;
import javafx.data.pull.PullParser;
import javafx.data.xml.QName;
import javafx.io.http.HttpRequest;
 
import java.io.InputStream;
import java.lang.Exception;
import java.net.URLEncoder;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.SwingWorker;
  
public class AtomReadWorker extends SwingWorker {
    public-init var url: String;
    public-init var thumbnailPane: ThumbnailPane;
 
    // サムネイルのURLを取り出すための正規表現
    def pattern = java.util.regex.Pattern.compile("http://farm.*_m.jpg");
 
    // 非同期に行われる処理
    public override function doInBackground(): Object {
        // HTTP通信を行う
        var request = HttpRequest {
            location: url
                     
            onInput: parseAtom;

            onException: function(ex: Exception) {
                println("exception: {ex.getMessage()}");
            }
        };
        request.enqueue();
    }
 
    // Atomのパース
    function parseAtom(stream: InputStream) {
        var thumbnail: Thumbnail;
        var inEntry: Boolean;
 
        var parser = PullParser {
            documentType: PullParser.XML;
            input: stream
 
            onEvent: function(event: Event) {
                if (event.type == PullParser.START_ELEMENT) {
                    if (event.qname.name == "entry") {
                        // <entry>の開始
                        // 新たなThumbnailオブジェクトを生成する
                        inEntry = true;
                        thumbnail = Thumbnail {};
                    }
                }
                if (event.type == PullParser.END_ELEMENT) {
                    if (event.qname.name == "entry") {
                        // </entry>の場合
                        // 作成したThumbnailオブジェクトをイベントディスパッチスレッドで
                        // 取得できるように、publishする
                       inEntry = false;
                        this.publish(thumbnail as Object);
                    } else if (inEntry and event.qname.name == "title") {
                        // </title>の場合
                        // Thumbnailオブジェクトのtitleを設定
                        thumbnail.title = event.text;
                    } else if (inEntry and event.qname.name == "content") {
                        // </content>の場合
                        // 正規表現を用いて、サムネイルのURLを取り出し、
                        // thumbnailUrlに設定
                        var matcher = pattern.matcher(event.text);
                        if (matcher.find()) {
                            var url = matcher.group();
                            thumbnail.thumbnailUrl = url.replaceAll("_m", "_t");
                            // 大きいイメージのURLを作成
                            thumbnail.imageUrl = url.replaceAll("_m", "");
                        }
                    }
                }
            }
        }
        // パースの開始
        parser.parse();
        
        stream.close();
    }
 
    // イベントディスパッチスレッドで処理される関数
    // publishされたオブジェクトが引数となる
    override function process(thumbnails: List) {
        for (thumbnail in thumbnails) {
            insert thumbnail as Thumbnail into thumbnailPane.thumbnails;
        }
    }
}