All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opalj.hermes.Visualization.scala Maven / Gradle / Ivy

/* BSD 2-Clause License - see OPAL/LICENSE for details. */
package org.opalj.hermes

import scala.collection.mutable
import scala.collection.mutable.ListBuffer
import scala.collection.mutable.ArrayBuffer
import java.io.File
import java.io.PrintWriter
import java.net.URL
import javafx.event.EventHandler
import javafx.scene.control.CheckBoxTreeItem
import javafx.beans.value.ChangeListener
import javafx.beans.value.ObservableValue
import javafx.concurrent.Worker.State

import netscape.javascript.JSObject
import play.api.libs.json.JsObject
import play.api.libs.json.Json

import scala.io.Source
import scalafx.Includes._
import scalafx.collections.ObservableBuffer
import scalafx.scene.Scene
import scalafx.scene.control.Button
import scalafx.scene.control.CheckMenuItem
import scalafx.scene.control.Menu
import scalafx.scene.control.MenuBar
import scalafx.scene.control.MenuItem
import scalafx.scene.control.RadioMenuItem
import scalafx.scene.control.ToggleGroup
import scalafx.scene.control.TreeView
import scalafx.scene.control.cell.CheckBoxTreeCell
import scalafx.scene.layout.HBox
import scalafx.scene.layout.Priority
import scalafx.scene.layout.StackPane
import scalafx.scene.layout.VBox
import scalafx.scene.web.WebEngine
import scalafx.scene.web.WebView
import scalafx.stage.FileChooser.ExtensionFilter
import scalafx.stage.FileChooser
import scalafx.stage.Screen
import scalafx.stage.Stage
import org.opalj.io.process
import org.opalj.io.processSource
import org.opalj.log.OPALLogger

/**
 * Uses a WebView for javascript-based visualizations of feature query results.
 *
 * @author Alexander Appel
 */
object Visualization {

    class SelectionOption(val name: String) {

        private[this] val options: ListBuffer[SelectionOption] = new ListBuffer[SelectionOption]

        def +=(name: String): ListBuffer[SelectionOption] = options += new SelectionOption(name)

        def +=(other: SelectionOption): ListBuffer[SelectionOption] = options += other

        def -=(name: String): ListBuffer[SelectionOption] = options -= find(name).get

        def find(name: String): Option[SelectionOption] = options.find(_.name == name)
    }

    class D3DataProvider(
            val featureMatrix:   ObservableBuffer[ProjectFeatures[URL]],
            val selection:       SelectionOption,
            val optionSelection: mutable.HashMap[String, Boolean]
    ) {
        implicit val logContext = org.opalj.log.GlobalLogContext

        def getSelectedProjects(
            accCountThreshold:  Int,
            accSingleDisplayed: Double,
            accTotalDisplayed:  Double
        ): JsObject = {
            val projectSelection: SelectionOption = selection.find("projects").get
            val statisticSelection: SelectionOption = selection.find("statistics").get
            val featureSelection: SelectionOption = selection.find("features").get
            val projects = ArrayBuffer.empty[JsObject]
            featureMatrix.filter(p ⇒ projectSelection.find(p.id.value).isDefined).foreach { project ⇒
                // reformat for sorting
                val statistics: Seq[(String, Double)] = project.projectConfiguration.statistics.toSeq
                val features: Seq[(String, Double)] = project.features.map { f ⇒
                    (f.value.id, f.value.count.asInstanceOf[Double])
                }
                val selectedValues = (statistics ++ features)
                    .filter(f ⇒ statisticSelection.find(f._1).isDefined
                        || featureSelection.find(f._1).isDefined)
                    .sortBy(_._2).reverse

                if (validateFeatures(project.id.value, features)) {
                    val children = ArrayBuffer.empty[JsObject]
                    val total: Double = selectedValues.map(_._2).sum

                    if (accCountThreshold > 0 && selectedValues.length > accCountThreshold) {
                        var sum: Double = 0d
                        var rest: Double = 0d
                        val restData = ArrayBuffer.empty[JsObject]
                        selectedValues foreach { value ⇒
                            if ((children.length < 2) || (value._2 >= total * accSingleDisplayed)
                                && (sum + value._2 <= total * accTotalDisplayed)) {
                                children += Json.obj(
                                    "id" → value._1,
                                    "value" → value._2
                                )
                                sum += value._2
                            } else {
                                restData += Json.obj(
                                    "id" → value._1.replace('\n', ' '),
                                    "value" → value._2
                                )
                            }
                        }
                        rest = total - sum
                        if (rest > 0) {
                            children += Json.obj(
                                "id" → "",
                                "value" → rest,
                                "metrics" → Json.toJson(restData.toSeq)
                            )
                        }
                    } else {
                        selectedValues foreach { value ⇒
                            children += Json.obj(
                                "id" → value._1,
                                "value" → value._2
                            )
                        }
                    }

                    projects += Json.obj(
                        "id" → project.id.value,
                        "value" → total,
                        "metrics" → Json.toJson(children.toSeq)
                    )
                }
            }
            Json.obj(
                "id" → "flare",
                "options" → Json.toJson(optionSelection),
                "children" → Json.toJson(projects.toSeq)
            )
        }

        def getSingleProject(projectName: String): JsObject = {
            val statisticSelection: SelectionOption = selection.find("statistics").get
            val featureSelection: SelectionOption = selection.find("features").get
            val project = featureMatrix.find(_.id.value == projectName).get
            val statistics: Seq[(String, Double)] = project.projectConfiguration.statistics.toSeq
            val features: Seq[(String, Double)] = project.features.map(f ⇒ {
                (f.value.id, f.value.count.asInstanceOf[Double])
            })
            val selectedValues = (statistics ++ features)
                .filter(f ⇒ statisticSelection.find(f._1).isDefined
                    || featureSelection.find(f._1).isDefined)

            val children = ArrayBuffer.empty[JsObject]
            if (validateFeatures(projectName, selectedValues)) {
                selectedValues foreach { value ⇒
                    children += Json.obj(
                        "id" → value._1,
                        "value" → value._2
                    )
                }
            }
            Json.obj(
                "id" → projectName,
                "options" → Json.toJson(optionSelection),
                "children" → Json.toJson(children.toSeq)
            )
        }

        private def validateFeatures(project: String, features: Seq[(String, Double)]): Boolean = {
            val result = features.find(f ⇒ f._2.isNaN)
            if (result.isDefined) {
                OPALLogger.info("project visualization", "removing project "+project+": found NaN value for "+result.get._1)
            }
            result.isEmpty
        }
    }

    private val scripts = Map[String, String](
        "BubbleChart" -> getClass.getResource("d3js/bubbleChart.js").getPath,
        "BubblePieChart" -> getClass.getResource("d3js/bubblePieChart.js").getPath
    )

    private class OptionDetails(val description: String, val selected: Boolean)

    private val options = Map[String, OptionDetails](
        "Bubbles" -> new OptionDetails("Show Bubble Labels", true),
        "Pies" -> new OptionDetails("Show Pie Labels", true)
    )

    def display(
        mainStage:     Stage,
        featureMatrix: ObservableBuffer[ProjectFeatures[URL]]
    ): Stage = new Stage() { stage ⇒
        title = "Query Result Visualization"
        private val screenBounds = Screen.primary.visualBounds
        private val scaling = 0.67
        scene = new Scene(screenBounds.width * scaling, screenBounds.height * scaling) {
            root = new VBox {
                // setup WebView
                val webView: WebView = new WebView { contextMenuEnabled = false }
                val webEngine: WebEngine = webView.engine
                val stackRight = new StackPane
                stackRight.children.add(webView)

                // setup bridge class object
                val rootSelection: SelectionOption = new SelectionOption("root")
                val labelOptions = new mutable.HashMap[String, Boolean]
                val dataProvider = new D3DataProvider(featureMatrix, rootSelection, labelOptions)

                // register bridge before loading a script
                webEngine.getLoadWorker.stateProperty.addListener(
                    new ChangeListener[State]() {
                        def changed(
                            obs:      ObservableValue[_ <: State],
                            oldState: State, newState: State
                        ): Unit = {
                            if (newState == State.SUCCEEDED) {
                                val bridge = webEngine.executeScript("window").asInstanceOf[JSObject]
                                bridge.setMember("provider", dataProvider)
                                val script = loadScript(getClass.getResource("d3js/bubbleChart.js").getPath)
                                webEngine.executeScript(script)
                                webEngine.executeScript("init();")
                                webEngine.executeScript("display();")
                            }
                        }
                    }
                )
                // load base canvas
                webEngine.load(getClass.getResource("d3js/canvas.html").toExternalForm)

                // build tree items for filter options
                val rootQueries = new CheckBoxTreeItem[String]("root") { setExpanded(true) }
                val filterItems = new CheckBoxTreeItem[String]("Filter") { setExpanded(true) }
                rootQueries.getChildren.add(filterItems)

                // statistics
                val project: ProjectFeatures[URL] = featureMatrix.get(0)
                val statisticSelection: SelectionOption = new SelectionOption("statistics")
                rootSelection += statisticSelection
                val projectStatistic = new CheckBoxTreeItem[String]("Statistics")
                project.projectConfiguration.statistics foreach { statistic ⇒
                    projectStatistic.getChildren.add(
                        factoryCheckBoxTreeItem(statistic._1, statisticSelection)
                    )
                }
                filterItems.getChildren.add(projectStatistic)

                // features
                val projectFeature = new CheckBoxTreeItem[String]("Features")
                val featureSelection: SelectionOption = new SelectionOption("features")
                rootSelection += featureSelection
                project.featureGroups foreach {
                    case (group, features) ⇒
                        val featureGroup = new CheckBoxTreeItem[String](group.id)
                        features foreach { feature ⇒
                            featureGroup.getChildren.add(
                                factoryCheckBoxTreeItem(feature.value.id, featureSelection)
                            )
                        }
                        projectFeature.getChildren.add(featureGroup)
                }
                filterItems.getChildren.add(projectFeature)

                val rootProjects = new CheckBoxTreeItem[String]("root") {
                    setExpanded(true)
                }

                // project names
                val projectSelection: SelectionOption = new SelectionOption("projects")
                rootSelection += projectSelection
                val projectItems = new CheckBoxTreeItem[String]("Projects") {
                    setExpanded(true)
                }
                featureMatrix foreach { project ⇒
                    projectItems.getChildren.add(
                        factoryCheckBoxTreeItem(project.id.value, projectSelection)
                    )
                }
                rootProjects.getChildren.add(projectItems)

                // setup tree view for projects
                val treeViewProjects: TreeView[String] = new TreeView[String]
                treeViewProjects.root = rootProjects
                treeViewProjects.showRoot = false
                treeViewProjects.cellFactory = CheckBoxTreeCell.forTreeView[String]
                // set default selection after registering children
                projectItems.setSelected(true)

                // setup tree view for queries
                val treeViewQueries: TreeView[String] = new TreeView[String]
                treeViewQueries.root = rootQueries
                treeViewQueries.showRoot = false
                treeViewQueries.cellFactory = CheckBoxTreeCell.forTreeView[String]
                // set default selection after registering children
                projectStatistic.setSelected(true)

                // apply button
                val applyButton = new Button("Apply Filter") {
                    maxWidth = Double.MaxValue
                    onAction = handle { webEngine.executeScript("display();") }
                }

                // setup menu bar
                val menuFile = new Menu("File")
                val menuOptions = new Menu("Options")
                val menuView = new Menu("View")

                // svg export
                val svgExport = new MenuItem("Export as SVG") {
                    onAction = handle {
                        val fileChooser = new FileChooser {
                            title = "Save as..."
                            extensionFilters ++= Seq(
                                new ExtensionFilter("Scalable Vector Graphics", "*.svg")
                            )
                        }
                        val selectedFile = fileChooser.showSaveDialog(mainStage)
                        if (selectedFile != null) {
                            val filename = selectedFile.getName
                            val extension = filename.substring(
                                filename.lastIndexOf("."),
                                filename.length()
                            )
                            if (extension.equals(".svg")) {
                                exportSVG(
                                    selectedFile,
                                    webEngine.executeScript("exportSVG();").asInstanceOf[String]
                                )
                            }
                        }
                    }
                }
                menuFile.items.addAll(svgExport)

                // label options
                menuOptions.items = options.map { item ⇒
                    val (name, options) = item
                    new CheckMenuItem(options.description) {
                        onAction = handle {
                            labelOptions += (name → selected.value)
                            webEngine.executeScript("display();")
                        }
                        if (options.selected) {
                            selected = options.selected
                            // manually add to labelOptions if default
                            labelOptions += (name → selected.value)
                        }
                    }
                }(collection.breakOut): List[CheckMenuItem]

                // view options
                val viewToggle = new ToggleGroup
                menuView.items = scripts.map { item ⇒
                    val (name, path) = item
                    new RadioMenuItem(name) {
                        toggleGroup = viewToggle
                        onAction = handle {
                            webEngine.executeScript(loadScript(path))
                            // re-init on view change
                            webEngine.executeScript("init();")
                            webEngine.executeScript("display();")
                            title = stage.title.value+" - "+text.value
                        }
                    }
                }(collection.breakOut): List[RadioMenuItem]
                viewToggle.toggles.get(0).setSelected(true)

                val menuBar = new MenuBar { menus.addAll(menuFile, menuOptions, menuView) }

                // build GUI
                val vBoxLeft = new VBox
                VBox.setVgrow(treeViewQueries, Priority.Always)
                vBoxLeft.children.addAll(applyButton, treeViewProjects, treeViewQueries)

                val mainContent = new HBox
                HBox.setHgrow(stackRight, Priority.Always)
                mainContent.children.addAll(vBoxLeft, stackRight)

                VBox.setVgrow(mainContent, Priority.Always)
                children.addAll(menuBar, mainContent)
            }
        }
        initOwner(mainStage)
    }

    private def exportSVG(file: File, svg: String): Unit = {
        process(new PrintWriter(file)) { _.write(svg) }
    }

    private def loadScript(file: String): String = {
        processSource(Source.fromFile(file)) { s ⇒ s.getLines().mkString("\n") }
    }

    private def factoryCheckBoxTreeItem(
        value:           String,
        selectionParent: SelectionOption
    ): CheckBoxTreeItem[String] = {
        new CheckBoxTreeItem[String](value) {
            addEventHandler(
                CheckBoxTreeItem.checkBoxSelectionChangedEvent[String],
                new EventHandler[CheckBoxTreeItem.TreeModificationEvent[String]] {
                    // gets called if a checkbox is updated
                    override def handle(event: CheckBoxTreeItem.TreeModificationEvent[String]): Unit = {
                        if (event.getTreeItem.isSelected) {
                            selectionParent += value
                        } else {
                            selectionParent -= value
                        }
                    }
                }
            )
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy