/*
* Copyright (C) 2011 Alasdair C. Hamilton
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*/
package ketUI;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import geom.*;
import ket.display.box.Box;
import ketUI.panel.KetPanel;
import ketUI.responder.Responder;
import ket.*;
import ket.math.*;
import ketUI.modes.*;
import ket.math.EquationList;
/**
* Record the path of the mouse and identify loops and their associated attributes.
*/
public class MouseGrid {
// TODO: Move to a separate class.
public class IdentityCounter {
IdentityHashMap<Argument,Double> map;
public IdentityCounter() {
map = new IdentityHashMap<Argument,Double>();
}
public double get(Argument key) {
Double d = map.get(key);
return d!=null ? d : 0.0;
}
public void put(Argument key, double value) {
map.put(key, value);
}
public void add(Argument key, double value) {
put(key, get(key)+value);
}
public void scale(Argument key, double value) {
put(key, get(key)*value);
}
public void increment(Argument key) {
add(key, 1);
}
public Set<Argument> keySet() {
return map.keySet();
}
public java.util.List<Argument> keyList() {
return new Vector<Argument>(map.keySet());
}
public void remove(Argument a) {
map.remove(a);
}
public void clear() {
map.clear();
}
public Argument calcMostFrequentArgument() {
Argument maxKey = null;
Double maxValue = null;
for (Argument key : map.keySet()) {
Double value = map.get(key);
if (maxKey==null || value>maxValue) {
maxKey = key;
maxValue = value;
}
}
return maxKey;
}
public void decay(double dt) {
for (Argument a : keyList()) {
scale(a, Math.pow(BASE, -dt/DECAY_SCALE));
}
}
// Remove elements less than a given value.
public void threshold(double min) {
for (Argument a : keyList()) {
double value = get(a);
if (value<min) {
remove(a);
} else {
put(a, value);
}
}
}
}
static double DECAY_SCALE = 500.0; // ms
static double BASE = 10.0;
static double BACKGROUND_NOISE_LEVEL = 0.05;
static final int MINIMUM_SAMPLE_SIZE = 6; // pixels
static final int GRID_SIZE = 10; // pixels
static final boolean SHOW_GRID = true;
IdentityHashMap<Argument,Box> boxMap;
IdentityCounter argumentFrequency;
Vector<Position> grid; //D
double time;
public MouseGrid() {
argumentFrequency = new IdentityCounter();
boxMap = new IdentityHashMap<Argument,Box>();
grid = new Vector<Position>();
time = System.currentTimeMillis();
}
public void paint(Graphics2D g2D) {
if (!SHOW_GRID || grid==null) return;
g2D.setColor(new Color(255, 0, 255));
for (Position p : grid) {
g2D.drawLine(1+(int) p.x, 1+(int) p.y, -1+(int) p.x, -1+(int) p.y);
g2D.drawLine(1+(int) p.x, -1+(int) p.y, -1+(int) p.x, +1+(int) p.y);
}
}
public void clear() {
grid.clear();
argumentFrequency.clear();
boxMap.clear();
}
/**
* Identify the selection based on what the loop circled.
*/
public Argument getMostFrequentArgument(Vector<Position> loop, boolean clockwise, KetPanel ketPanel) {
grid.clear();
removeInvalids();
argumentFrequency.decay(timeSinceLastCall());
argumentFrequency.threshold(BACKGROUND_NOISE_LEVEL);
calcArgumentFrequency(loop, clockwise, ketPanel);
return argumentFrequency.calcMostFrequentArgument();
}
private void removeInvalids() {
for (Argument a : argumentFrequency.keyList()) {
if (a.getEquationList()==null) {
argumentFrequency.remove(a);
boxMap.remove(a);
}
}
}
private double timeSinceLastCall() {
double t2 = System.currentTimeMillis();
double dt = t2 - time;
time = t2;
return dt;
}
/*
* Evenly sample points within the loop for the associated argument.
* TODO: This may made more efficient by finding the x_min and
* x_max for each y value and iterating at the original step
* size between these points.
*/
private void calcArgumentFrequency(Vector<Position> loop, boolean clockwise, KetPanel ketPanel) {
Position centre = Position.average(loop.firstElement(), loop.lastElement());
Offset step = getSpread(loop, centre);
step.scale(2.0/GRID_SIZE);
step.upperLimit(MINIMUM_SAMPLE_SIZE, MINIMUM_SAMPLE_SIZE);
IdentityCounter counter = new IdentityCounter();
double areaCircled = 0.0;
for (double x=centre.x-step.width; x<=centre.x+step.width; x+=step.width) {
for (double y=centre.y-step.height; y<=centre.y+step.height; y+=step.height) {
Position point = new Position(x, y);
if (!isWithinLoop(point, loop, clockwise, centre)) continue;
areaCircled += step.area();
grid.add(point);
Box box = ketPanel.findDeepestBox(point);
if (box==null) continue;
Argument argument = box.getArgument();
if (argument==null) continue;
counter.increment(argument);
if ( ! boxMap.containsKey(argument) ) {
boxMap.put(argument, box);
}
for (Branch a : argument.getAncestors()) {
counter.increment(a);
}
}
}
// Remove all keys associated with null boxes.
for (Argument argument : counter.keyList()) {
if (boxMap.get(argument)==null) {
//- System.out.println("[remove " + argument + "]");
counter.remove(argument);
}
}
// Scale grids by the size of each box so larger boxes require
// more area of the loop to be selected.
double norm = 0.0;
for (Argument argument : counter.keySet()) { // (box, argument, count)
Box box = boxMap.get(argument);
if (box==null) continue;
double count = counter.get(argument);
double selectedBoxArea = count*step.area();
double fracCircled = selectedBoxArea/areaCircled;
double fracBox = selectedBoxArea/box.getArea();
double weight = fracCircled * fracBox * selectedBoxArea;
/*-
double boxFrac = selectedBoxArea / box.getArea(); // Frac area of box.
//? double weight = boxFrac * selectedArea;
double weight = boxFrac * selectedBoxArea;
*/
counter.put(argument, weight);
norm += weight;
}
for (Argument argument : counter.keySet()) {
double value = counter.get(argument) / norm;
argumentFrequency.add(argument, value);
}
}
private Offset getSpread(Vector<Position> loop, Position centre) {
Offset spread = new Offset();
for (Position p : loop) {
double deltaX = Math.abs(centre.x-p.x);
double deltaY = Math.abs(centre.y-p.y);
if (spread.width<deltaX) {
spread.width = deltaX;
}
if (spread.height<deltaY) {
spread.height = deltaY;
}
}
return spread;
}
/**
* Determine if the given point Q lies within the loop by
* checking if its on the 'inside' of each line segment, AB, of the
* given loop.
*/
private boolean isWithinLoop(Position Q, Vector<Position> loop, boolean clockwise, Position centre) {
Position A = null;
for (Position B : loop) {
if (A==null) { // Skip the first iteration.
A = B;
continue;
}
if (Position.isLeftOfLine(Q, A, B)^clockwise) {
return false;
}
// Rotate line segment AB 180 degrees around centre and compare.
double dAx = A.x - centre.x;
double dAy = A.y - centre.y;
double dBx = B.x - centre.x;
double dBy = B.y - centre.y;
Position Arot = new Position(centre.x-dAx, centre.y-dAy);
Position Brot = new Position(centre.x-dBx, centre.y-dBy);
if (Position.isLeftOfLine(Q, Arot, Brot)^clockwise) {
return false;
}
A = B;
}
return true;
}
}