First commit, added all projects
This commit is contained in:
+28
@@ -0,0 +1,28 @@
|
||||
|
||||
|
||||
### Created by https://www.gitignore.io
|
||||
### Java ###
|
||||
# Compiled class file
|
||||
*.class
|
||||
|
||||
# Log file
|
||||
*.log
|
||||
|
||||
# BlueJ files
|
||||
*.ctxt
|
||||
|
||||
# Mobile Tools for Java (J2ME)
|
||||
.mtj.tmp/
|
||||
|
||||
# Package Files #
|
||||
*.jar
|
||||
*.war
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*
|
||||
replay_pid*
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<fileset-config file-format-version="1.2.0" simple-config="true" sync-formatter="false">
|
||||
<fileset name="all" enabled="true" check-config-name="CS480StyleGuide" local="false">
|
||||
<file-match-pattern match-pattern="." include-pattern="true"/>
|
||||
</fileset>
|
||||
</fileset-config>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-21">
|
||||
<attributes>
|
||||
<attribute name="module" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="src" path="src"/>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/5"/>
|
||||
<classpathentry kind="output" path="bin"/>
|
||||
</classpath>
|
||||
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>PA1</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>net.sf.eclipsecs.core.CheckstyleBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
<nature>net.sf.eclipsecs.core.CheckstyleNature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,66 @@
|
||||
package app;
|
||||
|
||||
import gui.*;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.*;
|
||||
import java.io.*;
|
||||
import javax.imageio.*;
|
||||
import javax.swing.*;
|
||||
|
||||
|
||||
/**
|
||||
* An app that can be used to test the CompletingField
|
||||
* and CompletingDocument classes.
|
||||
*
|
||||
* @author Prof. David Bernstein, James Madison University
|
||||
* @version 1.0
|
||||
*/
|
||||
public class CompletingFieldApp implements Runnable
|
||||
{
|
||||
private String fn;
|
||||
|
||||
/**
|
||||
* Explicit Value Constructor.
|
||||
*
|
||||
* @param fn The name of the file to use
|
||||
*/
|
||||
public CompletingFieldApp(final String fn)
|
||||
{
|
||||
this.fn = fn;
|
||||
}
|
||||
|
||||
/**
|
||||
* The code to execute in the event dispatch thread.
|
||||
*/
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
JFrame frame = new JFrame("CompletingFieldApp");
|
||||
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
JPanel contentPane = (JPanel)frame.getContentPane();
|
||||
|
||||
contentPane.setLayout(new BorderLayout());
|
||||
contentPane.setBackground(Color.WHITE);
|
||||
|
||||
try
|
||||
{
|
||||
BufferedImage image = ImageIO.read(new File("logoWay.gif"));
|
||||
JLabel logo = new JLabel(new ImageIcon(image));
|
||||
contentPane.add(logo, BorderLayout.CENTER);
|
||||
}
|
||||
catch (IOException ioe)
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
|
||||
CompletingField field = new CompletingField();
|
||||
|
||||
field.setWordList(fn);
|
||||
field.setBorder(BorderFactory.createTitledBorder("Street Name"));
|
||||
contentPane.add(field, BorderLayout.SOUTH);
|
||||
|
||||
frame.setSize(300,300);
|
||||
frame.setVisible(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package app;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import javax.swing.*;
|
||||
|
||||
/**
|
||||
* A driver that can be used to test the CompletingField
|
||||
* and CompletingDocument classes.
|
||||
*
|
||||
* @author Prof. David Bernstein, James Madison University
|
||||
* @version 1.0
|
||||
*/
|
||||
public class CompletingFieldDriver
|
||||
{
|
||||
/**
|
||||
* The entry point of the application.
|
||||
*
|
||||
* @param args The command line arguments
|
||||
* @throws InterruptedException if something goes wrong
|
||||
* @throws InvocationTargetException if something goes wrong
|
||||
*/
|
||||
public static void main(final String[] args)
|
||||
throws InterruptedException, InvocationTargetException
|
||||
{
|
||||
String fn = "street-names.txt";
|
||||
if (args.length == 1)
|
||||
{
|
||||
fn = args[0];
|
||||
}
|
||||
|
||||
SwingUtilities.invokeAndWait(new CompletingFieldApp(fn));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package gui;
|
||||
|
||||
import text.WordFinder;
|
||||
|
||||
import javax.swing.text.AttributeSet;
|
||||
import javax.swing.text.BadLocationException;
|
||||
import javax.swing.text.Document;
|
||||
import javax.swing.text.PlainDocument;
|
||||
|
||||
/**
|
||||
* A document containing an auto-completing field.
|
||||
*/
|
||||
public class CompletingDocument extends PlainDocument implements Document
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private CompletingField field = null;
|
||||
private WordFinder finder = null;
|
||||
|
||||
/**
|
||||
* @param field The auto-completing field
|
||||
*/
|
||||
public CompletingDocument(final CompletingField field)
|
||||
{
|
||||
this.field = field;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insertString(final int offset, final String s, final AttributeSet as)
|
||||
throws BadLocationException
|
||||
{
|
||||
if (s == null || s.isEmpty())
|
||||
return;
|
||||
|
||||
super.insertString(offset, s, as);
|
||||
|
||||
if (this.finder == null) return;
|
||||
|
||||
String currentText = this.getText(0, getLength());
|
||||
String match = this.finder.find(currentText);
|
||||
|
||||
if (match != null)
|
||||
{
|
||||
String af = match.substring(currentText.length());
|
||||
super.insertString(getLength(), af, as);
|
||||
this.field.select(currentText.length(), match.length());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param fileName The filename containing the list of words to auto-complete from
|
||||
*/
|
||||
public void setWordList(final String fileName)
|
||||
{
|
||||
this.finder = new WordFinder(fileName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package gui;
|
||||
|
||||
import javax.swing.JTextField;
|
||||
import javax.swing.text.Document;
|
||||
|
||||
|
||||
/**
|
||||
* An text field with the ability to set a list of words to attempt to auto-complete from.
|
||||
*/
|
||||
public class CompletingField extends JTextField
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
private CompletingDocument document;
|
||||
|
||||
@Override
|
||||
protected Document createDefaultModel()
|
||||
{
|
||||
document = new CompletingDocument(this);
|
||||
return document;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param fileName The name of a file to read a list of auto-complete candidates from
|
||||
*/
|
||||
public void setWordList(final String fileName)
|
||||
{
|
||||
document.setWordList(fileName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package testing;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.List;
|
||||
|
||||
import javax.swing.text.Document;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import gui.CompletingDocument;
|
||||
import gui.CompletingField;
|
||||
|
||||
class TestCompletingDocument
|
||||
{
|
||||
enum Prefix
|
||||
{
|
||||
CA("ca"),
|
||||
MA("ma"),
|
||||
C("c"),
|
||||
CARB("carb"),
|
||||
EMPTY("");
|
||||
|
||||
private final String value;
|
||||
|
||||
Prefix(final String value)
|
||||
{
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
String value()
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
enum Word
|
||||
{
|
||||
CAT("cat"),
|
||||
DOG("dog"),
|
||||
CAR("car"),
|
||||
CART("cart"),
|
||||
CARBON("carbon");
|
||||
|
||||
private final String value;
|
||||
|
||||
Word(final String value)
|
||||
{
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
String value()
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
enum FileLabel
|
||||
{
|
||||
WORDS("words.txt");
|
||||
|
||||
private final String value;
|
||||
|
||||
FileLabel(final String value)
|
||||
{
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
String value()
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@TempDir
|
||||
File tempDir;
|
||||
|
||||
private File writeWords(final List<Word> words) throws IOException
|
||||
{
|
||||
File file = new File(tempDir, FileLabel.WORDS.value());
|
||||
Files.write(
|
||||
file.toPath(),
|
||||
words.stream().map(Word::value).toList()
|
||||
);
|
||||
return file;
|
||||
}
|
||||
|
||||
@Test
|
||||
void insertingTextWorksWhenFinderIsUnset() throws Exception
|
||||
{
|
||||
CompletingField field = new CompletingField();
|
||||
CompletingDocument doc = new CompletingDocument(field);
|
||||
field.setDocument(doc);
|
||||
|
||||
assertDoesNotThrow(() ->
|
||||
doc.insertString(0, Prefix.MA.value(), null)
|
||||
);
|
||||
|
||||
assertEquals(Prefix.MA.value(), doc.getText(0, doc.getLength()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void nullInsertIsIgnored() throws Exception
|
||||
{
|
||||
Document doc = new CompletingField().getDocument();
|
||||
|
||||
doc.insertString(0, null, null);
|
||||
|
||||
assertEquals(0, doc.getLength());
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptyInsertIsIgnored() throws Exception
|
||||
{
|
||||
Document doc = new CompletingField().getDocument();
|
||||
|
||||
doc.insertString(0, Prefix.EMPTY.value(), null);
|
||||
|
||||
assertEquals(0, doc.getLength());
|
||||
}
|
||||
|
||||
@Test
|
||||
void noCompletionOccursWhenNoWordsMatch() throws Exception
|
||||
{
|
||||
File file = writeWords(List.of(Word.DOG));
|
||||
CompletingField field = new CompletingField();
|
||||
CompletingDocument doc = (CompletingDocument) field.getDocument();
|
||||
|
||||
field.setWordList(file.getAbsolutePath());
|
||||
doc.insertString(0, Prefix.CA.value(), null);
|
||||
|
||||
assertEquals(Prefix.CA.value(), doc.getText(0, doc.getLength()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void completionOccursForSingleMatch() throws Exception
|
||||
{
|
||||
File file = writeWords(List.of(Word.CAT));
|
||||
CompletingField field = new CompletingField();
|
||||
CompletingDocument doc = (CompletingDocument) field.getDocument();
|
||||
|
||||
field.setWordList(file.getAbsolutePath());
|
||||
doc.insertString(0, Prefix.C.value(), null);
|
||||
|
||||
assertEquals(Word.CAT.value(), doc.getText(0, doc.getLength()));
|
||||
assertEquals(1, field.getSelectionStart());
|
||||
assertEquals(3, field.getSelectionEnd());
|
||||
}
|
||||
|
||||
@Test
|
||||
void longestMatchingWordIsChosen() throws Exception
|
||||
{
|
||||
File file = writeWords(
|
||||
List.of(Word.CAR, Word.CART, Word.CARBON)
|
||||
);
|
||||
|
||||
CompletingField field = new CompletingField();
|
||||
CompletingDocument doc = (CompletingDocument) field.getDocument();
|
||||
|
||||
field.setWordList(file.getAbsolutePath());
|
||||
doc.insertString(0, Prefix.CARB.value(), null);
|
||||
|
||||
assertEquals(Word.CARBON.value(), doc.getText(0, doc.getLength()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void completionFallsBackToRawInsertWhenMatchIsNull() throws Exception
|
||||
{
|
||||
File file = writeWords(List.of(Word.DOG));
|
||||
CompletingField field = new CompletingField();
|
||||
CompletingDocument doc = (CompletingDocument) field.getDocument();
|
||||
|
||||
field.setWordList(file.getAbsolutePath());
|
||||
doc.insertString(0, Prefix.CA.value(), null);
|
||||
|
||||
assertEquals(Prefix.CA.value(), doc.getText(0, doc.getLength()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package testing;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.text.Document;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import gui.CompletingDocument;
|
||||
import gui.CompletingField;
|
||||
|
||||
class TestCompletingField
|
||||
{
|
||||
enum WordSource
|
||||
{
|
||||
STREETS("street-names.txt"),
|
||||
EMPTY_LIST("empty.txt");
|
||||
|
||||
private final String value;
|
||||
|
||||
WordSource(final String value)
|
||||
{
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
String value()
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
enum AssertionText
|
||||
{
|
||||
DOCUMENT_NULL("Document should not be null");
|
||||
|
||||
private final String value;
|
||||
|
||||
AssertionText(final String value)
|
||||
{
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
String value()
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
private CompletingField createFieldOnEdt() throws Exception
|
||||
{
|
||||
CompletingField[] holder = new CompletingField[1];
|
||||
|
||||
SwingUtilities.invokeAndWait(() ->
|
||||
{
|
||||
holder[0] = new CompletingField();
|
||||
});
|
||||
|
||||
return holder[0];
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultConstructionUsesCompletingDocumentModel() throws Exception
|
||||
{
|
||||
CompletingField field = createFieldOnEdt();
|
||||
|
||||
Document document = field.getDocument();
|
||||
|
||||
assertNotNull(document, AssertionText.DOCUMENT_NULL.value());
|
||||
assertInstanceOf(CompletingDocument.class, document);
|
||||
}
|
||||
|
||||
@Test
|
||||
void settingWordSourceKeepsDocumentInitialized() throws Exception
|
||||
{
|
||||
CompletingField field = createFieldOnEdt();
|
||||
|
||||
SwingUtilities.invokeAndWait(() ->
|
||||
{
|
||||
field.setWordList(WordSource.STREETS.value());
|
||||
});
|
||||
|
||||
assertNotNull(field.getDocument());
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleWordListAssignmentsDoNotBreakDocument() throws Exception
|
||||
{
|
||||
CompletingField field = createFieldOnEdt();
|
||||
|
||||
SwingUtilities.invokeAndWait(() ->
|
||||
{
|
||||
field.setWordList(WordSource.STREETS.value());
|
||||
field.setWordList(WordSource.EMPTY_LIST.value());
|
||||
});
|
||||
|
||||
assertNotNull(field.getDocument());
|
||||
}
|
||||
|
||||
@Test
|
||||
void documentRemainsCompletingDocumentAfterWordListChange() throws Exception
|
||||
{
|
||||
CompletingField field = createFieldOnEdt();
|
||||
|
||||
SwingUtilities.invokeAndWait(() ->
|
||||
{
|
||||
field.setWordList(WordSource.STREETS.value());
|
||||
});
|
||||
|
||||
assertInstanceOf(CompletingDocument.class, field.getDocument());
|
||||
}
|
||||
|
||||
@Test
|
||||
void callingSetWordListBeforeEdtCompletionDoesNotThrow() throws Exception
|
||||
{
|
||||
CompletingField[] holder = new CompletingField[1];
|
||||
|
||||
SwingUtilities.invokeAndWait(() ->
|
||||
{
|
||||
holder[0] = new CompletingField();
|
||||
holder[0].setWordList(WordSource.STREETS.value());
|
||||
});
|
||||
|
||||
assertNotNull(holder[0].getDocument());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package testing;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import text.WordFinder;
|
||||
|
||||
class TestWordFinder
|
||||
{
|
||||
enum FileName
|
||||
{
|
||||
DATA("data.txt"),
|
||||
MISSING("missing.txt"),
|
||||
SINGLE("single.txt"),
|
||||
ANIMALS("animals.txt"),
|
||||
WORDS("words.txt"),
|
||||
MIXED("mixed.txt");
|
||||
|
||||
private final String value;
|
||||
|
||||
FileName(final String value)
|
||||
{
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
String value()
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
enum Word
|
||||
{
|
||||
ANT("ant"),
|
||||
BEAR("bear"),
|
||||
TIGER("tiger"),
|
||||
|
||||
CAT("cat"),
|
||||
DOG("dog"),
|
||||
DEER("deer"),
|
||||
|
||||
CAMEL("camel"),
|
||||
CARROT("carrot"),
|
||||
CATNIP("catnip"),
|
||||
|
||||
|
||||
SUN("sun"),
|
||||
MOON("moon"),
|
||||
STARS("stars"),
|
||||
|
||||
ALPHA("alpha"),
|
||||
BETA("beta"),
|
||||
|
||||
NULL(null);
|
||||
|
||||
private final String value;
|
||||
|
||||
Word(final String value)
|
||||
{
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
String value()
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
enum Prefix
|
||||
{
|
||||
A("a"),
|
||||
CA("ca"),
|
||||
Z("z"),
|
||||
EMPTY(""),
|
||||
SPACE(" "),
|
||||
MULTI_SPACE(" ");
|
||||
|
||||
private final String value;
|
||||
|
||||
Prefix(final String value)
|
||||
{
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
String value()
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@TempDir
|
||||
File tempDir;
|
||||
|
||||
private File write(final FileName name, final List<Word> words) throws IOException
|
||||
{
|
||||
File file = new File(tempDir, name.value());
|
||||
Files.write(
|
||||
file.toPath(),
|
||||
words.stream().map(Word::value).toList()
|
||||
);
|
||||
return file;
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadsWordsAndSupportsPrefixSearch() throws Exception
|
||||
{
|
||||
File file = write(
|
||||
FileName.DATA,
|
||||
List.of(Word.TIGER, Word.ANT, Word.BEAR)
|
||||
);
|
||||
|
||||
WordFinder finder = new WordFinder(file.getAbsolutePath());
|
||||
|
||||
assertEquals(Word.ANT.value(), finder.find(Prefix.A.value()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingFileProducesNoResults() throws Exception
|
||||
{
|
||||
File missing = new File(tempDir, FileName.MISSING.value());
|
||||
WordFinder finder = new WordFinder(missing.getAbsolutePath());
|
||||
|
||||
assertNull(finder.find(Prefix.A.value()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void nullPrefixAlwaysReturnsNull() throws Exception
|
||||
{
|
||||
File file = write(
|
||||
FileName.SINGLE,
|
||||
List.of(Word.CAT)
|
||||
);
|
||||
|
||||
WordFinder finder = new WordFinder(file.getAbsolutePath());
|
||||
|
||||
assertNull(finder.find(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptyOrWhitespacePrefixesReturnNull() throws Exception
|
||||
{
|
||||
File file = write(
|
||||
FileName.ANIMALS,
|
||||
List.of(Word.DOG, Word.DEER)
|
||||
);
|
||||
|
||||
WordFinder finder = new WordFinder(file.getAbsolutePath());
|
||||
|
||||
assertAll(
|
||||
() -> assertNull(finder.find(Prefix.EMPTY.value())),
|
||||
() -> assertNull(finder.find(Prefix.SPACE.value())),
|
||||
() -> assertNull(finder.find(Prefix.MULTI_SPACE.value()))
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnsAlphabeticallyFirstMatchingWord() throws Exception
|
||||
{
|
||||
File file = write(
|
||||
FileName.WORDS,
|
||||
List.of(Word.CARROT, Word.CATNIP, Word.CAMEL)
|
||||
);
|
||||
|
||||
WordFinder finder = new WordFinder(file.getAbsolutePath());
|
||||
|
||||
assertEquals(Word.CAMEL.value(), finder.find(Prefix.CA.value()));
|
||||
|
||||
File file2 = write(
|
||||
FileName.WORDS,
|
||||
List.of(Word.CARROT, Word.CATNIP, Word.CAMEL, Word.BEAR)
|
||||
);
|
||||
|
||||
WordFinder finder2 = new WordFinder(file2.getAbsolutePath());
|
||||
|
||||
assertEquals(Word.CAMEL.value(), finder2.find(Prefix.CA.value()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnsNullWhenNoWordsMatchPrefix() throws Exception
|
||||
{
|
||||
File file = write(
|
||||
FileName.WORDS,
|
||||
List.of(Word.SUN, Word.MOON, Word.STARS)
|
||||
);
|
||||
|
||||
WordFinder finder = new WordFinder(file.getAbsolutePath());
|
||||
|
||||
assertNull(finder.find(Prefix.Z.value()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void ignoresNullEntriesInWordFile() throws Exception
|
||||
{
|
||||
File file = write(
|
||||
FileName.MIXED,
|
||||
List.of(Word.ALPHA, Word.NULL, Word.BETA)
|
||||
);
|
||||
|
||||
WordFinder finder = new WordFinder(file.getAbsolutePath());
|
||||
|
||||
assertEquals(Word.ALPHA.value(), finder.find(Prefix.A.value()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package text;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* A utility to find the first word in an alphabetically-sorted list that starts with an input
|
||||
* prefix.
|
||||
*/
|
||||
public class WordFinder
|
||||
{
|
||||
private List<String> words = null;
|
||||
|
||||
/**
|
||||
* @param fileName The file to read the list of words from
|
||||
*/
|
||||
public WordFinder(final String fileName)
|
||||
{
|
||||
Path filePath = Paths.get(fileName);
|
||||
try
|
||||
{
|
||||
this.words = Files.readAllLines(filePath).stream().map(s -> s.trim())
|
||||
.collect(Collectors.toList());
|
||||
Collections.sort(this.words);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param prefix The prefix to match against
|
||||
* @return The first word matching the prefix or null if no matches were found
|
||||
*/
|
||||
public String find(final String prefix)
|
||||
{
|
||||
if (prefix == null || prefix.isBlank() || this.words == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
int low = 0;
|
||||
int high = this.words.size() - 1;
|
||||
String lprefix = prefix.toLowerCase();
|
||||
|
||||
int validPrefixIndex = -1;
|
||||
|
||||
while (low <= high)
|
||||
{
|
||||
int mid = low + (high - low) / 2;
|
||||
|
||||
String midWord = this.words.get(mid);
|
||||
if (midWord.toLowerCase().startsWith(lprefix))
|
||||
{
|
||||
validPrefixIndex = mid;
|
||||
break;
|
||||
}
|
||||
|
||||
int comp = midWord.compareToIgnoreCase(lprefix);
|
||||
|
||||
if (comp < 0)
|
||||
{
|
||||
low = mid + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
high = mid - 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (validPrefixIndex == -1)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
while (validPrefixIndex > 0
|
||||
&& this.words.get(validPrefixIndex - 1).toLowerCase().startsWith(lprefix))
|
||||
{
|
||||
validPrefixIndex--;
|
||||
}
|
||||
|
||||
return this.words.get(validPrefixIndex);
|
||||
}
|
||||
}
|
||||
+62895
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-21">
|
||||
<attributes>
|
||||
<attribute name="module" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="src" path="src"/>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/5"/>
|
||||
<classpathentry kind="output" path="bin"/>
|
||||
</classpath>
|
||||
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>PA2</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>net.sf.eclipsecs.core.CheckstyleBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
<nature>net.sf.eclipsecs.core.CheckstyleNature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
@@ -0,0 +1,84 @@
|
||||
package app;
|
||||
|
||||
import gui.*;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
import java.awt.image.*;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.swing.*;
|
||||
|
||||
|
||||
/**
|
||||
* An application that can be used to digitize something
|
||||
* (in display coordinates).
|
||||
*
|
||||
* @author Prof. David Bernstein, James Madison University
|
||||
* @version 1.0
|
||||
*/
|
||||
public class DigitizerApp implements ActionListener, Runnable
|
||||
{
|
||||
private DigitizerPanel dp;
|
||||
private JRadioButton addButton, deleteButton;
|
||||
|
||||
/**
|
||||
* The code to run in the event dispatch thread.
|
||||
*/
|
||||
public void run()
|
||||
{
|
||||
// Setup the JFrame
|
||||
JFrame frame = new JFrame("Way - Digitizer");
|
||||
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
frame.setSize(700, 700);
|
||||
JPanel contentPane = (JPanel)frame.getContentPane();
|
||||
contentPane.setLayout(new BorderLayout());
|
||||
|
||||
// Setup the ortho photo
|
||||
BufferedImage ortho;
|
||||
try
|
||||
{
|
||||
ortho = ImageIO.read(new File("orthophoto.png"));
|
||||
}
|
||||
catch (IOException io)
|
||||
{
|
||||
ortho = null;
|
||||
}
|
||||
|
||||
// Setup the DigitizerPanel
|
||||
dp = new DigitizerPanel(ortho);
|
||||
contentPane.add(dp, BorderLayout.CENTER);
|
||||
|
||||
// Setup the opearing mode panel
|
||||
JPanel south = new JPanel();
|
||||
south.setLayout(new FlowLayout(FlowLayout.LEFT));
|
||||
south.setBorder(BorderFactory.createTitledBorder("Operating Mode"));
|
||||
addButton = new JRadioButton("Add Line", true);
|
||||
addButton.addActionListener(this);
|
||||
south.add(addButton);
|
||||
deleteButton = new JRadioButton("Delete Line", false);
|
||||
deleteButton.addActionListener(this);
|
||||
south.add(deleteButton);
|
||||
ButtonGroup modeGroup = new ButtonGroup();
|
||||
modeGroup.add(addButton);
|
||||
modeGroup.add(deleteButton);
|
||||
contentPane.add(south, BorderLayout.SOUTH);
|
||||
|
||||
frame.setVisible(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle actionPerformed messages. Specifically, change the
|
||||
* operating mode of the DigitizerPanel.
|
||||
*
|
||||
* @param evt The event that generated the message
|
||||
*/
|
||||
public void actionPerformed(final ActionEvent evt)
|
||||
{
|
||||
if (addButton.isSelected()) dp.setMode(DigitizerPanel.ADD);
|
||||
else dp.setMode(DigitizerPanel.DELETE);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package app;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import javax.swing.*;
|
||||
|
||||
/**
|
||||
* A driver that can be used to test the DigitizerApp.
|
||||
*
|
||||
* @author Prof. David Bernstein, James Madison University
|
||||
* @version 1.0
|
||||
*/
|
||||
public class DigitizerDriver
|
||||
{
|
||||
/**
|
||||
* The entry point of the application.
|
||||
*
|
||||
* @param args The command line arguments
|
||||
* @throws InterruptedException if something goes wrong
|
||||
* @throws InvocationTargetException if something goes wrong
|
||||
*/
|
||||
public static void main(final String[] args)
|
||||
throws InterruptedException, InvocationTargetException
|
||||
{
|
||||
SwingUtilities.invokeAndWait(new DigitizerApp());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package gui;
|
||||
|
||||
import java.awt.geom.Line2D;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public interface DigitizerDocument
|
||||
{
|
||||
/**
|
||||
* @param start
|
||||
* The start coordinates of the line (in display coordinates).
|
||||
* @param stop
|
||||
* The stop coordinates of the line (in display coordinates).
|
||||
*/
|
||||
public void addLine(double[] start, double[] stop);
|
||||
|
||||
/**
|
||||
* @param point The point to find the closest line to (in display coordinates).
|
||||
* @return The closest line to the given point, or null if there are no lines.
|
||||
*/
|
||||
public Line2D getClosest(double[] point);
|
||||
|
||||
/**
|
||||
* @return The lines in the model
|
||||
*/
|
||||
public List<Line2D.Double> getLines();
|
||||
|
||||
/**
|
||||
* @param line The line to remove from the model
|
||||
*/
|
||||
public void removeLine(Line2D line);
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package gui;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.event.MouseListener;
|
||||
import java.awt.event.MouseMotionListener;
|
||||
import java.awt.geom.Line2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.List;
|
||||
|
||||
import javax.swing.JPanel;
|
||||
|
||||
/**
|
||||
* A panel that can be used to digitize something (in display coordinates).
|
||||
*/
|
||||
public class DigitizerPanel extends JPanel implements MouseListener, MouseMotionListener
|
||||
{
|
||||
public static final int ADD = 0;
|
||||
public static final int DELETE = 1;
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private BufferedImage ortho;
|
||||
private DigitizerDocument model = null;
|
||||
private int mode;
|
||||
|
||||
private Line2D.Double currentLine = null;
|
||||
private boolean hasDragged = false;
|
||||
|
||||
/**
|
||||
* @param ortho
|
||||
*/
|
||||
public DigitizerPanel(final BufferedImage ortho)
|
||||
{
|
||||
this.ortho = ortho;
|
||||
this.model = createDefaultModel();
|
||||
this.mode = ADD;
|
||||
addMouseListener(this);
|
||||
addMouseMotionListener(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A default model for this panel
|
||||
*/
|
||||
public DigitizerDocument createDefaultModel()
|
||||
{
|
||||
model = new DisplayDigitizerDocument(this);
|
||||
return model;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The lines in the model
|
||||
*/
|
||||
public List<Line2D.Double> getLines()
|
||||
{
|
||||
return model.getLines();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param g
|
||||
* The graphics context to paint on
|
||||
*/
|
||||
public void paint(final Graphics g)
|
||||
{
|
||||
super.paint(g);
|
||||
g.drawImage(ortho, 0, 0, null);
|
||||
for (Line2D line : model.getLines())
|
||||
{
|
||||
g.setColor(Color.GREEN);
|
||||
g.drawLine((int) line.getX1(), (int) line.getY1(), (int) line.getX2(), (int) line.getY2());
|
||||
}
|
||||
if (currentLine != null)
|
||||
{
|
||||
g.setColor(Color.YELLOW);
|
||||
g.drawLine((int) currentLine.getX1(), (int) currentLine.getY1(), (int) currentLine.getX2(),
|
||||
(int) currentLine.getY2());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mode
|
||||
* The operating mode to set
|
||||
*/
|
||||
public void setMode(final int mode)
|
||||
{
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseDragged(final MouseEvent e)
|
||||
{
|
||||
hasDragged = true;
|
||||
if (mode == ADD && currentLine != null)
|
||||
{
|
||||
currentLine.setLine(currentLine.getX1(), currentLine.getY1(), e.getX(), e.getY());
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseMoved(final MouseEvent e)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseClicked(final MouseEvent e)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mousePressed(final MouseEvent e)
|
||||
{
|
||||
if (mode == ADD)
|
||||
{
|
||||
currentLine = new Line2D.Double(e.getX(), e.getY(), e.getX(), e.getY());
|
||||
System.out.println("Started line at " + e.getX() + ", " + e.getY());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseReleased(final MouseEvent e)
|
||||
{
|
||||
if (mode == ADD && currentLine != null && hasDragged)
|
||||
{
|
||||
model.addLine(new double[] {currentLine.getX1(), currentLine.getY1()},
|
||||
new double[] {currentLine.getX2(), currentLine.getY2()});
|
||||
currentLine = null;
|
||||
repaint();
|
||||
}
|
||||
else if (mode == DELETE && !hasDragged)
|
||||
{
|
||||
Line2D closest = model.getClosest(new double[] {e.getX(), e.getY()});
|
||||
if (closest != null)
|
||||
model.removeLine(closest);
|
||||
repaint();
|
||||
}
|
||||
hasDragged = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseEntered(final MouseEvent e)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseExited(final MouseEvent e)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package gui;
|
||||
|
||||
import java.awt.geom.Line2D;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A simple implementation of the DigitizerDocument interface that can be used to test the
|
||||
* DigitizerPanel.
|
||||
*/
|
||||
public class DisplayDigitizerDocument implements DigitizerDocument
|
||||
{
|
||||
protected DigitizerPanel panel;
|
||||
protected List<Line2D.Double> lines = new ArrayList<Line2D.Double>();
|
||||
|
||||
/**
|
||||
* @param panel
|
||||
*/
|
||||
public DisplayDigitizerDocument(final DigitizerPanel panel)
|
||||
{
|
||||
this.panel = panel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addLine(final double[] start, final double[] stop)
|
||||
{
|
||||
Line2D.Double line = new Line2D.Double(start[0], start[1], stop[0], stop[1]);
|
||||
lines.add(line);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Line2D getClosest(final double[] point)
|
||||
{
|
||||
Line2D closest = null;
|
||||
double closestDistance = Double.MAX_VALUE;
|
||||
for (Line2D line : lines)
|
||||
{
|
||||
double distance = line.ptSegDist(point[0], point[1]);
|
||||
if (distance < closestDistance)
|
||||
{
|
||||
closest = line;
|
||||
closestDistance = distance;
|
||||
}
|
||||
}
|
||||
return closest;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Line2D.Double> getLines()
|
||||
{
|
||||
return lines;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeLine(final Line2D line)
|
||||
{
|
||||
lines.remove(line);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package math;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class Vector
|
||||
{
|
||||
private Vector()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param v Vector 1
|
||||
* @param w Vector 2
|
||||
* @return The dot product of v and w
|
||||
*/
|
||||
public static double dot(final double[] v, final double[] w)
|
||||
{
|
||||
return v[0] * w[0] + v[1] * w[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param v Vector 1
|
||||
* @param w Vector 2
|
||||
* @return The difference of v and w (v - w)
|
||||
*/
|
||||
public static double[] minus(final double[] v, final double[] w)
|
||||
{
|
||||
return new double[] {v[0] - w[0], v[1] - w[1]};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param v Vector
|
||||
* @return The norm (length) of v
|
||||
*/
|
||||
public static double norm(final double[] v)
|
||||
{
|
||||
return Math.sqrt(dot(v, v));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param v Vector
|
||||
* @return The normalized version of v (a vector in the same direction with length 1)
|
||||
*/
|
||||
public static double[] normalize(final double[] v)
|
||||
{
|
||||
double n = norm(v);
|
||||
return new double[] {v[0] / n, v[1] / n};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param v Vector
|
||||
* @return A vector perpendicular to v (rotated 90 degrees counterclockwise)
|
||||
*/
|
||||
public static double[] perp(final double[] v)
|
||||
{
|
||||
return new double[] {-v[1], v[0]};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param v Vector 1
|
||||
* @param w Vector 2
|
||||
* @return The sum of v and w
|
||||
*/
|
||||
public static double[] plus(final double[] v, final double[] w)
|
||||
{
|
||||
return new double[] {v[0] + w[0], v[1] + w[1]};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param s Scalar
|
||||
* @param v Vector
|
||||
* @return The product of s and v (s * v)
|
||||
*/
|
||||
public static double[] times(final double s, final double[] v)
|
||||
{
|
||||
return new double[] {s * v[0], s * v[1]};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param v Vector
|
||||
* @param s Scalar
|
||||
* @return The product of v and s (v * s)
|
||||
*/
|
||||
public static double[] times(final double[] v, final double s)
|
||||
{
|
||||
return times(s, v);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
package testing;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import gui.DigitizerPanel;
|
||||
|
||||
import java.awt.Graphics;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.geom.Line2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class TestDigitizerPanel
|
||||
{
|
||||
private DigitizerPanel panel;
|
||||
private BufferedImage image;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp()
|
||||
{
|
||||
image = new BufferedImage(200, 200, BufferedImage.TYPE_INT_RGB);
|
||||
panel = new DigitizerPanel(image);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInitialLinesEmpty()
|
||||
{
|
||||
assertTrue(panel.getLines().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateDefaultModelReturnsNonNull()
|
||||
{
|
||||
assertNotNull(panel.createDefaultModel());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetLinesReturnsNonNull()
|
||||
{
|
||||
assertNotNull(panel.getLines());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetModeAdd()
|
||||
{
|
||||
assertDoesNotThrow(() -> panel.setMode(DigitizerPanel.ADD));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetModeDelete()
|
||||
{
|
||||
assertDoesNotThrow(() -> panel.setMode(DigitizerPanel.DELETE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddLineViaDragAddsToModel()
|
||||
{
|
||||
panel.setMode(DigitizerPanel.ADD);
|
||||
|
||||
mousePress(panel, 10, 20);
|
||||
mouseDrag(panel, 50, 60);
|
||||
mouseRelease(panel, 50, 60);
|
||||
|
||||
List<Line2D.Double> lines = panel.getLines();
|
||||
assertEquals(1, lines.size());
|
||||
Line2D line = lines.get(0);
|
||||
assertEquals(10.0, line.getX1(), 1e-9);
|
||||
assertEquals(20.0, line.getY1(), 1e-9);
|
||||
assertEquals(50.0, line.getX2(), 1e-9);
|
||||
assertEquals(60.0, line.getY2(), 1e-9);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReleaseWithoutDragDoesNotAddLine()
|
||||
{
|
||||
panel.setMode(DigitizerPanel.ADD);
|
||||
|
||||
mousePress(panel, 10, 20);
|
||||
mouseRelease(panel, 10, 20);
|
||||
|
||||
assertTrue(panel.getLines().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleLinesCanBeAdded()
|
||||
{
|
||||
panel.setMode(DigitizerPanel.ADD);
|
||||
|
||||
addLine(panel, 0, 0, 10, 10);
|
||||
addLine(panel, 20, 20, 40, 40);
|
||||
|
||||
assertEquals(2, panel.getLines().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteModeRemovesClosestLine()
|
||||
{
|
||||
panel.setMode(DigitizerPanel.ADD);
|
||||
addLine(panel, 0, 0, 100, 0);
|
||||
|
||||
panel.setMode(DigitizerPanel.DELETE);
|
||||
mousePress(panel, 50, 2);
|
||||
mouseRelease(panel, 50, 2);
|
||||
|
||||
assertTrue(panel.getLines().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteWithDragDoesNotRemoveLine()
|
||||
{
|
||||
panel.setMode(DigitizerPanel.ADD);
|
||||
addLine(panel, 0, 0, 100, 0);
|
||||
|
||||
panel.setMode(DigitizerPanel.DELETE);
|
||||
mousePress(panel, 50, 2);
|
||||
mouseDrag(panel, 55, 2);
|
||||
mouseRelease(panel, 55, 2);
|
||||
|
||||
assertEquals(1, panel.getLines().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteOnEmptyModelDoesNotThrow()
|
||||
{
|
||||
panel.setMode(DigitizerPanel.DELETE);
|
||||
assertDoesNotThrow(() ->
|
||||
{
|
||||
mousePress(panel, 50, 50);
|
||||
mouseRelease(panel, 50, 50);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPaintDoesNotThrow()
|
||||
{
|
||||
panel.setMode(DigitizerPanel.ADD);
|
||||
addLine(panel, 10, 10, 50, 50);
|
||||
|
||||
panel.setSize(200, 200);
|
||||
|
||||
BufferedImage canvas = new BufferedImage(200, 200, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics g = canvas.getGraphics();
|
||||
|
||||
assertDoesNotThrow(() -> panel.paint(g));
|
||||
|
||||
g.dispose();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPaintWithActiveCurrentLineDoesNotThrow()
|
||||
{
|
||||
panel.setSize(200, 200);
|
||||
panel.setMode(DigitizerPanel.ADD);
|
||||
|
||||
mousePress(panel, 10, 10);
|
||||
|
||||
BufferedImage canvas = new BufferedImage(200, 200, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics g = canvas.getGraphics();
|
||||
|
||||
assertDoesNotThrow(() -> panel.paint(g));
|
||||
|
||||
g.dispose();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMouseMovedDoesNotThrow()
|
||||
{
|
||||
assertDoesNotThrow(() -> panel.mouseMoved(fakeEvent(panel, 5, 5)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMouseClickedDoesNotThrow()
|
||||
{
|
||||
assertDoesNotThrow(() -> panel.mouseClicked(fakeEvent(panel, 5, 5)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMouseEnteredDoesNotThrow()
|
||||
{
|
||||
assertDoesNotThrow(() -> panel.mouseEntered(fakeEvent(panel, 5, 5)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMouseExitedDoesNotThrow()
|
||||
{
|
||||
assertDoesNotThrow(() -> panel.mouseExited(fakeEvent(panel, 5, 5)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddConstantValue()
|
||||
{
|
||||
assertEquals(0, DigitizerPanel.ADD);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteConstantValue()
|
||||
{
|
||||
assertEquals(1, DigitizerPanel.DELETE);
|
||||
}
|
||||
|
||||
private MouseEvent fakeEvent(final DigitizerPanel source, final int x, final int y)
|
||||
{
|
||||
return new MouseEvent(source, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(), 0, x, y, 1,
|
||||
false);
|
||||
}
|
||||
|
||||
private void mousePress(final DigitizerPanel p, final int x, final int y)
|
||||
{
|
||||
p.mousePressed(fakeEvent(p, x, y));
|
||||
}
|
||||
|
||||
private void mouseDrag(final DigitizerPanel p, final int x, final int y)
|
||||
{
|
||||
p.mouseDragged(fakeEvent(p, x, y));
|
||||
}
|
||||
|
||||
private void mouseRelease(final DigitizerPanel p, final int x, final int y)
|
||||
{
|
||||
p.mouseReleased(fakeEvent(p, x, y));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param p
|
||||
* @param x1
|
||||
* @param y1
|
||||
* @param x2
|
||||
* @param y2
|
||||
*/
|
||||
private void addLine(final DigitizerPanel p, final int x1, final int y1, final int x2,
|
||||
final int y2)
|
||||
{
|
||||
mousePress(p, x1, y1);
|
||||
mouseDrag(p, x2, y2);
|
||||
mouseRelease(p, x2, y2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package testing;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import gui.DigitizerPanel;
|
||||
import gui.DisplayDigitizerDocument;
|
||||
|
||||
import java.awt.geom.Line2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class TestDisplayDigitizerDocument
|
||||
{
|
||||
private DisplayDigitizerDocument doc;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp()
|
||||
{
|
||||
BufferedImage image = new BufferedImage(200, 200, BufferedImage.TYPE_INT_RGB);
|
||||
DigitizerPanel panel = new DigitizerPanel(image);
|
||||
doc = new DisplayDigitizerDocument(panel);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInitialLinesEmpty()
|
||||
{
|
||||
assertTrue(doc.getLines().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetLinesReturnsNonNull()
|
||||
{
|
||||
assertNotNull(doc.getLines());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddLineSingleLine()
|
||||
{
|
||||
doc.addLine(new double[] {1.0, 2.0}, new double[] {3.0, 4.0});
|
||||
assertEquals(1, doc.getLines().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddLineCoordinatesStoredCorrectly()
|
||||
{
|
||||
doc.addLine(new double[] {1.0, 2.0}, new double[] {3.0, 4.0});
|
||||
Line2D line = doc.getLines().get(0);
|
||||
assertEquals(1.0, line.getX1(), 1e-9);
|
||||
assertEquals(2.0, line.getY1(), 1e-9);
|
||||
assertEquals(3.0, line.getX2(), 1e-9);
|
||||
assertEquals(4.0, line.getY2(), 1e-9);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddMultipleLines()
|
||||
{
|
||||
doc.addLine(new double[] {0, 0}, new double[] {1, 1});
|
||||
doc.addLine(new double[] {2, 2}, new double[] {3, 3});
|
||||
doc.addLine(new double[] {4, 4}, new double[] {5, 5});
|
||||
assertEquals(3, doc.getLines().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddLineWithZeroLength()
|
||||
{
|
||||
doc.addLine(new double[] {5.0, 5.0}, new double[] {5.0, 5.0});
|
||||
assertEquals(1, doc.getLines().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddLineNegativeCoordinates()
|
||||
{
|
||||
doc.addLine(new double[] {-10.0, -20.0}, new double[] {-5.0, -15.0});
|
||||
Line2D line = doc.getLines().get(0);
|
||||
assertEquals(-10.0, line.getX1(), 1e-9);
|
||||
assertEquals(-20.0, line.getY1(), 1e-9);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoveLineDecreasesSize()
|
||||
{
|
||||
doc.addLine(new double[] {0, 0}, new double[] {10, 10});
|
||||
Line2D line = doc.getLines().get(0);
|
||||
doc.removeLine(line);
|
||||
assertTrue(doc.getLines().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoveLineRemovesCorrectOne()
|
||||
{
|
||||
doc.addLine(new double[] {0, 0}, new double[] {1, 1});
|
||||
doc.addLine(new double[] {5, 5}, new double[] {6, 6});
|
||||
|
||||
Line2D first = doc.getLines().get(0);
|
||||
doc.removeLine(first);
|
||||
|
||||
List<Line2D.Double> remaining = doc.getLines();
|
||||
assertEquals(1, remaining.size());
|
||||
assertEquals(5.0, remaining.get(0).getX1(), 1e-9);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoveNonExistentLineDoesNotThrow()
|
||||
{
|
||||
Line2D ghost = new Line2D.Double(99, 99, 100, 100);
|
||||
assertDoesNotThrow(() -> doc.removeLine(ghost));
|
||||
assertTrue(doc.getLines().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetClosestOnEmptyReturnsNull()
|
||||
{
|
||||
assertNull(doc.getClosest(new double[] {5.0, 5.0}));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetClosestSingleLine()
|
||||
{
|
||||
doc.addLine(new double[] {0, 0}, new double[] {10, 0});
|
||||
Line2D result = doc.getClosest(new double[] {5.0, 1.0});
|
||||
assertNotNull(result);
|
||||
assertEquals(0.0, result.getX1(), 1e-9);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetClosestPicksNearerLine()
|
||||
{
|
||||
doc.addLine(new double[] {0, 0}, new double[] {10, 0});
|
||||
doc.addLine(new double[] {0, 100}, new double[] {10, 100});
|
||||
|
||||
Line2D closest = doc.getClosest(new double[] {5.0, 2.0});
|
||||
assertNotNull(closest);
|
||||
assertEquals(0.0, closest.getY1(), 1e-9);
|
||||
assertEquals(0.0, closest.getY2(), 1e-9);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetClosestPointOnLine()
|
||||
{
|
||||
doc.addLine(new double[] {0, 0}, new double[] {10, 0});
|
||||
doc.addLine(new double[] {0, 50}, new double[] {10, 50});
|
||||
|
||||
Line2D closest = doc.getClosest(new double[] {5.0, 0.0});
|
||||
assertEquals(0.0, closest.getY1(), 1e-9);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetClosestAfterRemoval()
|
||||
{
|
||||
doc.addLine(new double[] {0, 0}, new double[] {10, 0});
|
||||
doc.addLine(new double[] {0, 50}, new double[] {10, 50});
|
||||
|
||||
Line2D near = doc.getClosest(new double[] {5, 0});
|
||||
doc.removeLine(near);
|
||||
|
||||
Line2D next = doc.getClosest(new double[] {5, 0});
|
||||
assertNotNull(next);
|
||||
assertEquals(50.0, next.getY1(), 1e-9);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetLinesReflectsAdditions()
|
||||
{
|
||||
List<Line2D.Double> lines = doc.getLines();
|
||||
doc.addLine(new double[] {0, 0}, new double[] {1, 1});
|
||||
assertEquals(1, lines.size());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
package testing;
|
||||
|
||||
import math.Vector;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
|
||||
/**
|
||||
* JUnit tests for the Vector class.
|
||||
*/
|
||||
class TestVector
|
||||
{
|
||||
private static final double DELTA = 1e-9;
|
||||
|
||||
// ─── dot ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
public void testDotBasic()
|
||||
{
|
||||
double[] v = {1.0, 2.0};
|
||||
double[] w = {3.0, 4.0};
|
||||
assertEquals(11.0, Vector.dot(v, w), DELTA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDotWithZeroVector()
|
||||
{
|
||||
double[] v = {5.0, 7.0};
|
||||
double[] zero = {0.0, 0.0};
|
||||
assertEquals(0.0, Vector.dot(v, zero), DELTA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDotOrthogonalVectors()
|
||||
{
|
||||
double[] v = {1.0, 0.0};
|
||||
double[] w = {0.0, 1.0};
|
||||
assertEquals(0.0, Vector.dot(v, w), DELTA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDotNegativeComponents()
|
||||
{
|
||||
double[] v = {-2.0, 3.0};
|
||||
double[] w = {4.0, -1.0};
|
||||
assertEquals(-11.0, Vector.dot(v, w), DELTA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDotCommutativity()
|
||||
{
|
||||
double[] v = {3.0, 5.0};
|
||||
double[] w = {2.0, -4.0};
|
||||
assertEquals(Vector.dot(v, w), Vector.dot(w, v), DELTA);
|
||||
}
|
||||
|
||||
// ─── minus ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
public void testMinusBasic()
|
||||
{
|
||||
double[] v = {5.0, 7.0};
|
||||
double[] w = {2.0, 3.0};
|
||||
double[] result = Vector.minus(v, w);
|
||||
assertArrayEquals(new double[] {3.0, 4.0}, result, DELTA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMinusSelf()
|
||||
{
|
||||
double[] v = {3.0, 9.0};
|
||||
double[] result = Vector.minus(v, v);
|
||||
assertArrayEquals(new double[] {0.0, 0.0}, result, DELTA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMinusNegativeComponents()
|
||||
{
|
||||
double[] v = {-1.0, -2.0};
|
||||
double[] w = {-3.0, -4.0};
|
||||
assertArrayEquals(new double[] {2.0, 2.0}, Vector.minus(v, w), DELTA);
|
||||
}
|
||||
|
||||
// ─── norm ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
public void testNormUnitVector()
|
||||
{
|
||||
double[] v = {1.0, 0.0};
|
||||
assertEquals(1.0, Vector.norm(v), DELTA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNorm34Triangle()
|
||||
{
|
||||
double[] v = {3.0, 4.0};
|
||||
assertEquals(5.0, Vector.norm(v), DELTA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNormZeroVector()
|
||||
{
|
||||
double[] v = {0.0, 0.0};
|
||||
assertEquals(0.0, Vector.norm(v), DELTA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNormNegativeComponents()
|
||||
{
|
||||
double[] v = {-3.0, -4.0};
|
||||
assertEquals(5.0, Vector.norm(v), DELTA);
|
||||
}
|
||||
|
||||
// ─── normalize ───────────────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
public void testNormalizeResultHasUnitNorm()
|
||||
{
|
||||
double[] v = {3.0, 4.0};
|
||||
double[] result = Vector.normalize(v);
|
||||
assertEquals(1.0, Vector.norm(result), DELTA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNormalizeDirection()
|
||||
{
|
||||
double[] v = {3.0, 4.0};
|
||||
double[] result = Vector.normalize(v);
|
||||
assertArrayEquals(new double[] {0.6, 0.8}, result, DELTA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNormalizeAlreadyUnit()
|
||||
{
|
||||
double[] v = {1.0, 0.0};
|
||||
assertArrayEquals(new double[] {1.0, 0.0}, Vector.normalize(v), DELTA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNormalizeNegativeComponents()
|
||||
{
|
||||
double[] v = {0.0, -5.0};
|
||||
assertArrayEquals(new double[] {0.0, -1.0}, Vector.normalize(v), DELTA);
|
||||
}
|
||||
|
||||
// ─── perp ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
public void testPerpBasic()
|
||||
{
|
||||
double[] v = {3.0, 4.0};
|
||||
assertArrayEquals(new double[] {-4.0, 3.0}, Vector.perp(v), DELTA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPerpIsOrthogonal()
|
||||
{
|
||||
double[] v = {2.0, 5.0};
|
||||
assertEquals(0.0, Vector.dot(v, Vector.perp(v)), DELTA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPerpPreservesNorm()
|
||||
{
|
||||
double[] v = {3.0, 4.0};
|
||||
assertEquals(Vector.norm(v), Vector.norm(Vector.perp(v)), DELTA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPerpTwiceIsNegation()
|
||||
{
|
||||
double[] v = {2.0, -3.0};
|
||||
double[] twice = Vector.perp(Vector.perp(v));
|
||||
assertArrayEquals(new double[] {-v[0], -v[1]}, twice, DELTA);
|
||||
}
|
||||
|
||||
// ─── plus ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
public void testPlusBasic()
|
||||
{
|
||||
double[] v = {1.0, 2.0};
|
||||
double[] w = {3.0, 4.0};
|
||||
assertArrayEquals(new double[] {4.0, 6.0}, Vector.plus(v, w), DELTA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPlusWithZero()
|
||||
{
|
||||
double[] v = {7.0, -3.0};
|
||||
double[] zero = {0.0, 0.0};
|
||||
assertArrayEquals(v, Vector.plus(v, zero), DELTA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPlusCommutativity()
|
||||
{
|
||||
double[] v = {1.0, 5.0};
|
||||
double[] w = {-2.0, 3.0};
|
||||
assertArrayEquals(Vector.plus(v, w), Vector.plus(w, v), DELTA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPlusAndMinusAreInverse()
|
||||
{
|
||||
double[] v = {4.0, 7.0};
|
||||
double[] w = {1.0, 3.0};
|
||||
assertArrayEquals(v, Vector.plus(Vector.minus(v, w), w), DELTA);
|
||||
}
|
||||
|
||||
// ─── times ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
public void testTimesScalarFirst()
|
||||
{
|
||||
double[] v = {2.0, 3.0};
|
||||
assertArrayEquals(new double[] {4.0, 6.0}, Vector.times(2.0, v), DELTA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTimesVectorFirst()
|
||||
{
|
||||
double[] v = {2.0, 3.0};
|
||||
assertArrayEquals(new double[] {4.0, 6.0}, Vector.times(v, 2.0), DELTA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTimesBothOverloadsConsistent()
|
||||
{
|
||||
double[] v = {5.0, -1.0};
|
||||
assertArrayEquals(Vector.times(3.0, v), Vector.times(v, 3.0), DELTA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTimesZeroScalar()
|
||||
{
|
||||
double[] v = {5.0, 9.0};
|
||||
assertArrayEquals(new double[] {0.0, 0.0}, Vector.times(0.0, v), DELTA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTimesNegativeScalar()
|
||||
{
|
||||
double[] v = {3.0, -4.0};
|
||||
assertArrayEquals(new double[] {-3.0, 4.0}, Vector.times(-1.0, v), DELTA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTimesScaleNorm()
|
||||
{
|
||||
double[] v = {3.0, 4.0};
|
||||
double s = 3.0;
|
||||
assertEquals(s * Vector.norm(v), Vector.norm(Vector.times(s, v)), DELTA);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-21">
|
||||
<attributes>
|
||||
<attribute name="module" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="src" path="src"/>
|
||||
<classpathentry kind="output" path="bin"/>
|
||||
</classpath>
|
||||
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>PA3</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>net.sf.eclipsecs.core.CheckstyleBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
<nature>net.sf.eclipsecs.core.CheckstyleNature</nature>
|
||||
</natures>
|
||||
<filteredResources>
|
||||
<filter>
|
||||
<id>1774216368666</id>
|
||||
<name></name>
|
||||
<type>30</type>
|
||||
<matcher>
|
||||
<id>org.eclipse.core.resources.regexFilterMatcher</id>
|
||||
<arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
|
||||
</matcher>
|
||||
</filter>
|
||||
</filteredResources>
|
||||
</projectDescription>
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../assets/rockingham-streets-2024.geo
|
||||
@@ -0,0 +1,102 @@
|
||||
package app;
|
||||
|
||||
import geography.*;
|
||||
import gui.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
import java.awt.print.*;
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import javax.swing.*;
|
||||
|
||||
public class PA3App implements ActionListener, Runnable
|
||||
{
|
||||
private JFileChooser fileChooser;
|
||||
private JFrame f;
|
||||
private CartographyPanel panel;
|
||||
|
||||
public void actionPerformed(ActionEvent evt)
|
||||
{
|
||||
String ac = evt.getActionCommand();
|
||||
|
||||
if (ac.equals("Open"))
|
||||
{
|
||||
int retval = fileChooser.showOpenDialog(f);
|
||||
if (retval == JFileChooser.APPROVE_OPTION)
|
||||
{
|
||||
String name = fileChooser.getSelectedFile().getName();
|
||||
AbstractMapProjection proj;
|
||||
if (name.indexOf("world") >= 0)
|
||||
proj = new SinusoidalProjection();
|
||||
else
|
||||
proj = new ConicalEqualAreaProjection(-96.0, 37.5, 29.5, 45.5);
|
||||
// proj = new SinusoidalProjection();
|
||||
try
|
||||
{
|
||||
FileInputStream in = new FileInputStream(name);
|
||||
GeographicShapesReader reader = new GeographicShapesReader(in, proj);
|
||||
CartographyDocument<GeographicShape> model = reader.read();
|
||||
panel.setModel(model);
|
||||
}
|
||||
catch (FileNotFoundException fnfe)
|
||||
{
|
||||
JOptionPane.showMessageDialog(f, "Unable to open file!", "Error",
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ac.equals("Exit"))
|
||||
{
|
||||
f.dispose();
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
public void run()
|
||||
{
|
||||
f = new JFrame();
|
||||
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
f.setSize(1920, 1080);
|
||||
|
||||
fileChooser = new JFileChooser();
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
InputStream in = new FileInputStream(new File("world-countries.geo"));
|
||||
AbstractMapProjection proj = new ConicalEqualAreaProjection(-96.0, 37.5, 29.5, 45.5);
|
||||
GeographicShapesReader reader = new GeographicShapesReader(in, proj);
|
||||
CartographyDocument<GeographicShape> model = reader.read();
|
||||
|
||||
panel = new CartographyPanel<GeographicShape>(model,
|
||||
new GeographicShapeCartographer(Color.BLACK));
|
||||
|
||||
JPanel cp = (JPanel) f.getContentPane();
|
||||
cp.setLayout(new BorderLayout());
|
||||
cp.add(panel, BorderLayout.CENTER);
|
||||
|
||||
JMenuBar menuBar = new JMenuBar();
|
||||
JMenuItem item;
|
||||
JMenu menu;
|
||||
|
||||
menu = new JMenu("File");
|
||||
menuBar.add(menu);
|
||||
item = new JMenuItem("Open");
|
||||
item.addActionListener(this);
|
||||
menu.add(item);
|
||||
|
||||
item = new JMenuItem("Exit");
|
||||
item.addActionListener(this);
|
||||
menu.add(item);
|
||||
|
||||
f.setJMenuBar(menuBar);
|
||||
f.setVisible(true);
|
||||
}
|
||||
catch (IOException ioe)
|
||||
{
|
||||
JOptionPane.showMessageDialog(f, ioe.toString(), "Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package app;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import javax.swing.*;
|
||||
|
||||
/**
|
||||
* A driver that can be used to test the CompletingField
|
||||
* and CompletingDocument classes.
|
||||
*
|
||||
* @author Prof. David Bernstein, James Madison University
|
||||
* @version 1.0
|
||||
*/
|
||||
public class PA3Driver
|
||||
{
|
||||
/**
|
||||
* The entry point of the application.
|
||||
*
|
||||
* @param args The command line arguments
|
||||
* @throws InterruptedException if something goes wrong
|
||||
* @throws InvocationTargetException if something goes wrong
|
||||
*/
|
||||
public static void main(final String[] args)
|
||||
throws InterruptedException, InvocationTargetException
|
||||
{
|
||||
|
||||
SwingUtilities.invokeAndWait(new PA3App());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package geography;
|
||||
|
||||
import java.awt.Shape;
|
||||
|
||||
/**
|
||||
* A GeographicShape is a shape that can be associated with a geographic location.
|
||||
*/
|
||||
public abstract class AbstractGeographicShape implements GeographicShape
|
||||
{
|
||||
private String id;
|
||||
|
||||
|
||||
/**
|
||||
* @param id The ID of the shape.
|
||||
*/
|
||||
public AbstractGeographicShape(final String id)
|
||||
{
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getID()
|
||||
{
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public abstract Shape getShape();
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package geography;
|
||||
/**
|
||||
* An abstract class that is extended to construct different types
|
||||
* map projections.
|
||||
*
|
||||
* @author Prof. David Bernstein, James Madison University
|
||||
* @version 1.0
|
||||
*/
|
||||
public abstract class AbstractMapProjection implements MapProjection
|
||||
{
|
||||
// Radius of spherical Earth (in km)
|
||||
protected static final double R = 6369.0;
|
||||
|
||||
protected static final double PI = Math.PI;
|
||||
protected static final double TWO_PI = 2.0*Math.PI;
|
||||
protected static final double PI_OVER_TWO = Math.PI/2.0;
|
||||
protected static final double PI_OVER_FOUR = Math.PI/4.0;
|
||||
|
||||
protected static final double RADIANS_PER_DEGREE = 2.0*Math.PI/360.0;
|
||||
|
||||
protected static final double DEGREES_PER_RADIAN = 1.0 / RADIANS_PER_DEGREE;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* The forward transformation (i.e., from Longitude/Latitude in
|
||||
* __degrees__ to kilometers above the equator and to the west of the
|
||||
* reference meridian).
|
||||
*
|
||||
* @param ll The longitude and latitude in __degrees__ (in that order)
|
||||
* @return KMs west of reference and north of equator (in that order)
|
||||
*/
|
||||
@Override
|
||||
public double[] forward(final double[] ll)
|
||||
{
|
||||
double lambda = ll[0] * RADIANS_PER_DEGREE;
|
||||
double phi = ll[1] * RADIANS_PER_DEGREE;
|
||||
|
||||
return forward(lambda, phi);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* The forward transformation (i.e., from Longitude/Latitude in
|
||||
* __radians__ to kilometers above the equator and to the west of the
|
||||
* reference meridian).
|
||||
*
|
||||
* @param lambda The longitude in __radians__
|
||||
* @param phi The latitude in __radians__
|
||||
* @return KMs west of reference and north of equator (in that order)
|
||||
*/
|
||||
@Override
|
||||
public abstract double[] forward(final double lambda, final double phi);
|
||||
|
||||
|
||||
/**
|
||||
* The inverse transformation from kilometers (above the
|
||||
* equator and to the west of the reference meridian)
|
||||
* to Longitude/Latitude in __degrees__.
|
||||
*
|
||||
* @param km The point in above,west coordinates (in that order)
|
||||
* @return The longitude and latitude in __degrees__ (in that order)
|
||||
*/
|
||||
@Override
|
||||
public double[] inverse(final double[] km)
|
||||
{
|
||||
double[] ll = inverse(km[0], km[1]);// In radians
|
||||
ll[0] = ll[0] * DEGREES_PER_RADIAN; // In degrees
|
||||
ll[1] = ll[1] * DEGREES_PER_RADIAN; // In degrees
|
||||
|
||||
return ll;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The inverse transformation from kilometers (above the
|
||||
* equator and to the west of the reference meridian)
|
||||
* to Longitude/Latitude in __radians__.
|
||||
*
|
||||
* @param ew The distance east/west in kilometers
|
||||
* @param ns The distance north/south in kilometers
|
||||
* @return The longitude and latitude in __radians__ (in that order)
|
||||
*/
|
||||
@Override
|
||||
public abstract double[] inverse(final double ew, final double ns);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package geography;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class ConicalEqualAreaProjection extends AbstractMapProjection
|
||||
{
|
||||
private double lamda0;
|
||||
private double phi0;
|
||||
private double phi1;
|
||||
private double phi2;
|
||||
|
||||
private double n;
|
||||
private double c;
|
||||
private double rho0;
|
||||
|
||||
/**
|
||||
* @param refM The reference meridian (in degrees)
|
||||
* @param refP The reference parallel (in degrees)
|
||||
* @param refP1 The first standard parallel (in degrees)
|
||||
* @param refP2 The second standard parallel (in degrees)
|
||||
*/
|
||||
public ConicalEqualAreaProjection(final double refM, final double refP, final double refP1,
|
||||
final double refP2)
|
||||
{
|
||||
this.lamda0 = refM * RADIANS_PER_DEGREE;
|
||||
this.phi0 = refP * RADIANS_PER_DEGREE;
|
||||
this.phi1 = refP1 * RADIANS_PER_DEGREE;
|
||||
this.phi2 = refP2 * RADIANS_PER_DEGREE;
|
||||
|
||||
n = 0.5 * (Math.sin(phi1) + Math.sin(phi2));
|
||||
c = Math.cos(phi1) * Math.cos(phi1) + 2.0 * n * Math.sin(phi1);
|
||||
rho0 = Math.sqrt(c - 2.0 * n * Math.sin(phi0)) / n;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double[] forward(final double lambda, final double phi)
|
||||
{
|
||||
double rho = Math.sqrt(c - 2.0 * n * Math.sin(phi)) / n;
|
||||
double theta = n * (lambda - lamda0);
|
||||
|
||||
return new double[] {R * rho * Math.sin(theta), R * (rho0 - rho * Math.cos(theta))};
|
||||
}
|
||||
|
||||
@Override
|
||||
public double[] inverse(final double ew, final double ns)
|
||||
{
|
||||
double a = Math.sqrt(Math.pow((ew / R), 2) + Math.pow((rho0 - ns / R), 2));
|
||||
double b = Math.atan((ew / R) / (rho0 - ns / R));
|
||||
|
||||
return new double[] {lamda0 + b / n, Math.asin((c - Math.pow(a * n, 2)) / (2.0 * n))};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package geography;
|
||||
|
||||
import java.awt.Shape;
|
||||
|
||||
/**
|
||||
* A GeographicShape is a shape that can be associated with a geographic location.
|
||||
*/
|
||||
public interface GeographicShape
|
||||
{
|
||||
/**
|
||||
* @return The ID of the shape.
|
||||
*/
|
||||
public String getID();
|
||||
|
||||
/**
|
||||
* @return The shape.
|
||||
*/
|
||||
public Shape getShape();
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package geography;
|
||||
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import gui.CartographyDocument;
|
||||
|
||||
/**
|
||||
* A class for reading geographic shapes from an input stream and creating a CartographyDocument
|
||||
* containing those shapes.
|
||||
*/
|
||||
public class GeographicShapesReader
|
||||
{
|
||||
private BufferedReader in;
|
||||
private MapProjection proj;
|
||||
|
||||
/**
|
||||
* @param is
|
||||
* The input stream to read the shapes from
|
||||
* @param proj
|
||||
* The map projection to use when converting geographic coordinates to Cartesian
|
||||
* coordinates
|
||||
*/
|
||||
public GeographicShapesReader(final InputStream is, final MapProjection proj)
|
||||
{
|
||||
this.in = new BufferedReader(new InputStreamReader(is));
|
||||
this.proj = proj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A CartographyDocument containing the shapes read from the input stream.
|
||||
*/
|
||||
public CartographyDocument<GeographicShape> read()
|
||||
{
|
||||
double maxX = Double.NEGATIVE_INFINITY;
|
||||
double maxY = Double.NEGATIVE_INFINITY;
|
||||
double minX = Double.POSITIVE_INFINITY;
|
||||
double minY = Double.POSITIVE_INFINITY;
|
||||
|
||||
Map<String, GeographicShape> shapes = new HashMap<>();
|
||||
|
||||
String currentLine;
|
||||
try
|
||||
{
|
||||
while ((currentLine = in.readLine()) != null)
|
||||
{
|
||||
Map<String, String> parsed = new LinkedHashMap<>();
|
||||
String[] tokens = currentLine.trim().split("(?=Type:|ID:|Code:)");
|
||||
|
||||
for (String token : tokens)
|
||||
{
|
||||
if (token.isBlank())
|
||||
continue;
|
||||
int colonIdx = token.indexOf(':');
|
||||
String key = token.substring(0, colonIdx).trim();
|
||||
String value = token.substring(colonIdx + 1).trim();
|
||||
parsed.put(key, value);
|
||||
}
|
||||
|
||||
String type = parsed.get("Type");
|
||||
String id = parsed.get("ID");
|
||||
|
||||
PiecewiseLinearCurve shape;
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case "PiecewiseLinearCurve":
|
||||
shape = new PiecewiseLinearCurve(id);
|
||||
break;
|
||||
case "Polygon":
|
||||
shape = new Polygon(id);
|
||||
break;
|
||||
case "Comment":
|
||||
in.readLine(); // skip the END
|
||||
continue;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
while ((currentLine = in.readLine()) != null)
|
||||
{
|
||||
currentLine = currentLine.trim();
|
||||
|
||||
if (currentLine.equals("END"))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
String[] strCoords = currentLine.trim().split("\\s+");
|
||||
double[] coords = new double[] {Double.parseDouble(strCoords[0]),
|
||||
Double.parseDouble(strCoords[1])};
|
||||
|
||||
double[] projCoords = proj.forward(coords);
|
||||
shape.add(projCoords);
|
||||
|
||||
maxX = Math.max(maxX, projCoords[0]);
|
||||
maxY = Math.max(maxY, projCoords[1]);
|
||||
minX = Math.min(minX, projCoords[0]);
|
||||
minY = Math.min(minY, projCoords[1]);
|
||||
}
|
||||
shapes.put(id, shape);
|
||||
}
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
Rectangle2D.Double bounds = new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY);
|
||||
return new CartographyDocument<>(shapes, bounds);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package geography;
|
||||
|
||||
/**
|
||||
* The requirements of a map projection.
|
||||
*
|
||||
* @author Prof. David Bernstein, James Madison University
|
||||
* @version 1.0
|
||||
*/
|
||||
public interface MapProjection
|
||||
{
|
||||
|
||||
/**
|
||||
* The forward transformation (i.e., from Longitude/Latitude in
|
||||
* __degrees__ to kilometers above the equator and to the west of the
|
||||
* reference meridian).
|
||||
*
|
||||
* @param ll The longitude and latitude in __degrees__ (in that order)
|
||||
* @return KMs west of reference and north of equator (in that order)
|
||||
*/
|
||||
public abstract double[] forward(double[] ll);
|
||||
|
||||
|
||||
/**
|
||||
* The forward transformation (i.e., from Longitude/Latitude in
|
||||
* __radians__ to kilometers above the equator and to the west of the
|
||||
* reference meridian).
|
||||
*
|
||||
* @param lambda The longitude in __radians__
|
||||
* @param phi The latitude in __radians__
|
||||
* @return KMs west of reference and north of equator (in that order)
|
||||
*/
|
||||
public abstract double[] forward(double lambda, double phi);
|
||||
|
||||
|
||||
/**
|
||||
* The inverse transformation from kilometers (above the
|
||||
* equator and to the west of the reference meridian)
|
||||
* to Longitude/Latitude in __degrees__.
|
||||
*
|
||||
* @param km The point in above,west coordinates (in that order)
|
||||
* @return The longitude and latitude in __degrees__ (in that order)
|
||||
*/
|
||||
public abstract double[] inverse(double[] km);
|
||||
|
||||
/**
|
||||
* The inverse transformation from kilometers (above the
|
||||
* equator and to the west of the reference meridian)
|
||||
* to Longitude/Latitude in __radians__.
|
||||
*
|
||||
* @param ew The distance east/west in kilometers
|
||||
* @param ns The distance north/south in kilometers
|
||||
* @return The longitude and latitude in __radians__ (in that order)
|
||||
*/
|
||||
public abstract double[] inverse(double ew, double ns);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package geography;
|
||||
|
||||
import java.awt.Shape;
|
||||
import java.awt.geom.Path2D;
|
||||
|
||||
/**
|
||||
* A class representing a piecewise linear curve.
|
||||
*/
|
||||
public class PiecewiseLinearCurve extends AbstractGeographicShape
|
||||
{
|
||||
protected Path2D.Double shape;
|
||||
|
||||
/**
|
||||
* @param id
|
||||
* The id of the curve
|
||||
*/
|
||||
public PiecewiseLinearCurve(final String id)
|
||||
{
|
||||
super(id);
|
||||
shape = new Path2D.Double();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id
|
||||
* The id of the curve
|
||||
* @param path
|
||||
* The path of the curve
|
||||
*/
|
||||
public PiecewiseLinearCurve(final String id, final Path2D.Double path)
|
||||
{
|
||||
super(id);
|
||||
this.shape = path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Shape getShape()
|
||||
{
|
||||
return shape;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param point
|
||||
* The point to add to the curve
|
||||
*/
|
||||
public void add(final double[] point)
|
||||
{
|
||||
if (shape.getCurrentPoint() == null)
|
||||
{
|
||||
shape.moveTo(point[0], point[1]);
|
||||
}
|
||||
else
|
||||
{
|
||||
shape.lineTo(point[0], point[1]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param addition
|
||||
* The shape to add to the curve
|
||||
* @param connect
|
||||
* Whether to connect the current curve to the addition with a line segment (if true, the
|
||||
* addition will be connected to the current curve with a line segment; if false, the
|
||||
* addition will be added as a separate shape)
|
||||
*/
|
||||
public void append(final Shape addition, final boolean connect)
|
||||
{
|
||||
if (connect && shape.getCurrentPoint() != null)
|
||||
{
|
||||
shape.lineTo(addition.getBounds2D().getX(), addition.getBounds2D().getY());
|
||||
}
|
||||
shape.append(addition, false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package geography;
|
||||
|
||||
import java.awt.Shape;
|
||||
import java.awt.geom.Path2D;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class Polygon extends PiecewiseLinearCurve
|
||||
{
|
||||
/**
|
||||
* @param id The id of the polygon
|
||||
*/
|
||||
public Polygon(final String id)
|
||||
{
|
||||
super(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id The id of the polygon
|
||||
* @param path The path of the polygon
|
||||
*/
|
||||
public Polygon(final String id, final Path2D.Double path)
|
||||
{
|
||||
super(id, path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Shape getShape()
|
||||
{
|
||||
shape.closePath();
|
||||
return shape;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package geography;
|
||||
|
||||
/**
|
||||
* A map projection that maps the globe onto a plane using the sinusoidal projection.
|
||||
*/
|
||||
public class SinusoidalProjection extends AbstractMapProjection
|
||||
{
|
||||
|
||||
@Override
|
||||
public double[] forward(final double lambda, final double phi)
|
||||
{
|
||||
return new double[] {R * lambda * Math.cos(phi), R * phi};
|
||||
}
|
||||
|
||||
@Override
|
||||
public double[] inverse(final double ew, final double ns)
|
||||
{
|
||||
double phi = ns / R;
|
||||
return new double[] {ew / (R * Math.cos(phi)), phi};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package gui;
|
||||
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.geom.AffineTransform;
|
||||
|
||||
/**
|
||||
* @param <T> The type of elements in the document
|
||||
*/
|
||||
public interface Cartographer<T>
|
||||
{
|
||||
/**
|
||||
* @param model The document to paint
|
||||
* @param g2 The graphics context to paint on
|
||||
* @param at The affine transform to apply to the graphics context before painting
|
||||
*/
|
||||
public void paintHighlights(final CartographyDocument<T> model, final Graphics2D g2,
|
||||
final AffineTransform at);
|
||||
|
||||
/**
|
||||
* @param model The document to paint
|
||||
* @param g2 The graphics context to paint on
|
||||
* @param at The affine transform to apply to the graphics context before painting
|
||||
*/
|
||||
public void paintShapes(final CartographyDocument<T> model, final Graphics2D g2,
|
||||
final AffineTransform at);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package gui;
|
||||
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @param <T> The type of elements in the document
|
||||
*/
|
||||
public class CartographyDocument<T> implements Iterable<T>
|
||||
{
|
||||
private Map<String, T> highlighted;
|
||||
private Map<String, T> elements;
|
||||
private Rectangle2D.Double bounds;
|
||||
|
||||
/**
|
||||
* @param elements The elements in the document, mapped by their id
|
||||
* @param bounds The bounds of the document
|
||||
*/
|
||||
public CartographyDocument(final Map<String, T> elements, final Rectangle2D.Double bounds)
|
||||
{
|
||||
this.elements = elements;
|
||||
this.bounds = bounds;
|
||||
this.highlighted = Map.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The bounds of the document
|
||||
*/
|
||||
public Rectangle2D.Double getBounds()
|
||||
{
|
||||
return bounds;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id The id of the element to get
|
||||
* @return The element with the given id, or null if no such element exists
|
||||
*/
|
||||
public T getElement(final String id)
|
||||
{
|
||||
return elements.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The iterator of highlighted elements in the document
|
||||
*/
|
||||
public Iterable<T> highlighted()
|
||||
{
|
||||
return highlighted.values();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The iterator of all elements in the document
|
||||
*/
|
||||
public Iterator<T> iterator()
|
||||
{
|
||||
return elements.values().iterator();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param highlighted The new highlighted elements, mapped by their id
|
||||
*/
|
||||
public void setHighlighted(final Map<String, T> highlighted)
|
||||
{
|
||||
this.highlighted = highlighted;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
package gui;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
import java.awt.geom.*;
|
||||
import java.util.*;
|
||||
|
||||
import math.*;
|
||||
|
||||
/**
|
||||
* A GUI component that can be extended to draw maps of various kinds.
|
||||
*
|
||||
* @param <T> The type of the data
|
||||
*
|
||||
* @author Prof. David Bernstein, James Madison University
|
||||
* @version 1.0
|
||||
*/
|
||||
public class CartographyPanel<T> extends JPanel implements MouseListener, MouseMotionListener
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
protected DisplayCoordinatesTransformation displayTransform;
|
||||
protected LinkedList<Rectangle2D.Double> zoomStack;
|
||||
|
||||
private CartographyDocument<T> model;
|
||||
private Cartographer<T> cartographer;
|
||||
private int[] rbMax, rbMin, rbStart, rbStop; // For the rubber-band-box
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Explicit Value Constructor.
|
||||
*
|
||||
* @param model The model to use
|
||||
* @param cartographer The cartographer to use
|
||||
*/
|
||||
public CartographyPanel(final CartographyDocument<T> model,
|
||||
final Cartographer<T> cartographer)
|
||||
{
|
||||
displayTransform = new DisplayCoordinatesTransformation();
|
||||
|
||||
this.model = model;
|
||||
zoomStack = new LinkedList<Rectangle2D.Double>();
|
||||
zoomStack.add(model.getBounds());
|
||||
|
||||
this.cartographer = cartographer;
|
||||
|
||||
rbStart = new int[2];
|
||||
rbStop = new int[2];
|
||||
rbMax = new int[2];
|
||||
rbMin = new int[2];
|
||||
|
||||
addMouseListener(this);
|
||||
addMouseMotionListener(this);
|
||||
|
||||
setDoubleBuffered(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a de-selection event (i.e., a zoom-out).
|
||||
*/
|
||||
public void handleDeselect()
|
||||
{
|
||||
if (zoomStack.size() > 1)
|
||||
{
|
||||
zoomStack.removeFirst();
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a selection event (i.e., a zoom-in).
|
||||
*
|
||||
* @param min The upper-left of the selection (in screen coordinates)
|
||||
* @param max The lower-right of the selection (in screen coordinates)
|
||||
*/
|
||||
protected void handleSelect(final double[] min, final double[] max)
|
||||
{
|
||||
double scale = displayTransform.getLastTransform().getScaleX();
|
||||
double translateX = displayTransform.getLastTransform().getTranslateX();
|
||||
double translateY = displayTransform.getLastTransform().getTranslateY();
|
||||
|
||||
double x = (min[0] - translateX) / scale;
|
||||
double y = (min[1] - translateY) / scale;
|
||||
double width = (max[0] - translateX) / scale - x;
|
||||
double height = (max[1] - translateY) / scale - y;
|
||||
|
||||
Rectangle2D.Double r = new Rectangle2D.Double(x, y, width, height);
|
||||
zoomStack.addFirst((Rectangle2D.Double)
|
||||
(displayTransform.getLastReflection().createTransformedShape(r).getBounds2D()));
|
||||
repaint();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the zoom stack.
|
||||
*
|
||||
* @param bounds The initial bounds
|
||||
*/
|
||||
private void initializeZoomStack(final Rectangle2D.Double bounds)
|
||||
{
|
||||
zoomStack = new LinkedList<Rectangle2D.Double>();
|
||||
zoomStack.add(bounds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle mouseClicked events.
|
||||
*
|
||||
* @param evt The MouseEvent
|
||||
*/
|
||||
@Override
|
||||
public void mouseClicked(final MouseEvent evt)
|
||||
{
|
||||
if (evt.getClickCount() > 1)
|
||||
{
|
||||
handleDeselect();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle mouseDragged events.
|
||||
*
|
||||
* @param evt The MouseEvent
|
||||
*/
|
||||
@Override
|
||||
public void mouseDragged(final MouseEvent evt)
|
||||
{
|
||||
int over = evt.getX();
|
||||
int down = evt.getY();
|
||||
Graphics g = getGraphics();
|
||||
|
||||
setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
|
||||
|
||||
// Erase the old rubber-band-box
|
||||
g.setXORMode(getBackground());
|
||||
g.setColor(Color.green);
|
||||
g.drawRect(rbMin[0], rbMin[1], rbMax[0]-rbMin[0], rbMax[1]-rbMin[1]);
|
||||
|
||||
// Calculate the coordinates of the new rubber-band-box
|
||||
rbStop[0] = over;
|
||||
rbStop[1] = down;
|
||||
rbMin[0] = Math.min(rbStart[0],rbStop[0]);
|
||||
rbMin[1] = Math.min(rbStart[1],rbStop[1]);
|
||||
rbMax[0] = Math.max(rbStart[0],rbStop[0]);
|
||||
rbMax[1] = Math.max(rbStart[1],rbStop[1]);
|
||||
|
||||
// Draw the new rubber-band-box
|
||||
g.drawRect(rbMin[0], rbMin[1], rbMax[0]-rbMin[0], rbMax[1]-rbMin[1]);
|
||||
g.setPaintMode();
|
||||
g.dispose();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Handle mouseEntered events.
|
||||
*
|
||||
* @param evt The MouseEvent
|
||||
*/
|
||||
@Override
|
||||
public void mouseEntered(final MouseEvent evt)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle mouseExited events.
|
||||
*
|
||||
* @param evt The MouseEvent
|
||||
*/
|
||||
@Override
|
||||
public void mouseExited(final MouseEvent evt)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle mouseMoved events.
|
||||
*
|
||||
* @param evt The MouseEvent
|
||||
*/
|
||||
@Override
|
||||
public void mouseMoved(final MouseEvent evt)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle mousePressed events.
|
||||
*
|
||||
* @param evt The MouseEvent
|
||||
*/
|
||||
@Override
|
||||
public void mousePressed(final MouseEvent evt)
|
||||
{
|
||||
int over = evt.getX();
|
||||
int down = evt.getY();
|
||||
|
||||
rbStart[0] = over;
|
||||
rbStart[1] = down;
|
||||
rbStop[0] = over;
|
||||
rbStop[1] = down;
|
||||
rbMin[0] = over;
|
||||
rbMin[1] = down;
|
||||
rbMax[0] = over;
|
||||
rbMax[1] = down;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle mouseReleased events.
|
||||
*
|
||||
* @param evt The MouseEvent
|
||||
*/
|
||||
@Override
|
||||
public void mouseReleased(final MouseEvent evt)
|
||||
{
|
||||
Graphics g = getGraphics();
|
||||
Dimension d = getSize();
|
||||
g.setClip(0, 0, d.width, d.height);
|
||||
|
||||
|
||||
// Erase the old line
|
||||
g.setXORMode(getBackground());
|
||||
g.setColor(Color.green);
|
||||
g.drawRect(rbMin[0], rbMin[1], rbMax[0]-rbMin[0], rbMax[1]-rbMin[1]);
|
||||
|
||||
// Determine the selected region
|
||||
double[] min = new double[2];
|
||||
min[0] = (double)(rbMin[0]);
|
||||
min[1] = (double)(rbMin[1] - 1);
|
||||
|
||||
double[] max = new double[2];
|
||||
max[0] = (double)(rbMax[0]);
|
||||
max[1] = (double)(rbMax[1] - 1);
|
||||
|
||||
|
||||
// Reset the graphics environment
|
||||
g.setPaintMode();
|
||||
g.dispose();
|
||||
setCursor(Cursor.getDefaultCursor());
|
||||
|
||||
// Notify any children of the selection
|
||||
if ((min[0] != max[0]) || (min[1] != max[1]))
|
||||
{
|
||||
handleSelect(min, max);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Render this component.
|
||||
*
|
||||
* @param g The rendering engine to use
|
||||
*/
|
||||
@Override
|
||||
public void paint(final Graphics g)
|
||||
{
|
||||
Graphics2D g2 = (Graphics2D)g;
|
||||
Rectangle screenBounds = g2.getClipBounds();
|
||||
g2.setColor(getBackground());
|
||||
g2.fill(screenBounds);
|
||||
g2.setColor(Color.BLACK);
|
||||
|
||||
Rectangle2D.Double bounds = zoomStack.getFirst();
|
||||
|
||||
AffineTransform at = displayTransform.getTransform(screenBounds, bounds);
|
||||
|
||||
cartographer.paintShapes(model, g2, at);
|
||||
cartographer.paintHighlights(model, g2, at);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the model.
|
||||
*
|
||||
* @param model The model
|
||||
*/
|
||||
public void setModel(final CartographyDocument<T> model)
|
||||
{
|
||||
this.model = model;
|
||||
initializeZoomStack(model.getBounds());
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package gui;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.geom.AffineTransform;
|
||||
|
||||
import geography.GeographicShape;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class GeographicShapeCartographer implements Cartographer<GeographicShape>
|
||||
{
|
||||
private Color color;
|
||||
|
||||
/**
|
||||
* @param color The color to use when rendering the shapes.
|
||||
*/
|
||||
public GeographicShapeCartographer(final Color color)
|
||||
{
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void paintHighlights(final CartographyDocument<GeographicShape> model, final Graphics2D g2,
|
||||
final AffineTransform at)
|
||||
{
|
||||
g2.setColor(new Color(color.getRed(), color.getGreen(), color.getBlue(), 128));
|
||||
for (GeographicShape shape : model.highlighted())
|
||||
{
|
||||
g2.fill(at.createTransformedShape(shape.getShape()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void paintShapes(final CartographyDocument<GeographicShape> model, final Graphics2D g2,
|
||||
final AffineTransform at)
|
||||
{
|
||||
g2.setColor(color);
|
||||
for (GeographicShape shape : model)
|
||||
{
|
||||
g2.draw(at.createTransformedShape(shape.getShape()));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package math;
|
||||
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
|
||||
/**
|
||||
* A view transformation that maps content coordinates to display coordinates, while maintaining the
|
||||
* aspect ratio of the content. The content is reflected across the horizontal axis to match the
|
||||
* typical display coordinate system where the y-axis increases downwards. The transformation also
|
||||
* centers the content within the display bounds.
|
||||
*/
|
||||
public class DisplayCoordinatesTransformation implements ViewTransformation
|
||||
{
|
||||
private AffineTransform lastReflection = new AffineTransform();
|
||||
private AffineTransform lastTransform = new AffineTransform();
|
||||
|
||||
@Override
|
||||
public AffineTransform getLastReflection()
|
||||
{
|
||||
return new AffineTransform(lastReflection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AffineTransform getLastTransform()
|
||||
{
|
||||
return new AffineTransform(lastTransform);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AffineTransform getTransform(final Rectangle2D displayBounds,
|
||||
final Rectangle2D contentBounds)
|
||||
{
|
||||
double scaleX = displayBounds.getWidth() / contentBounds.getWidth();
|
||||
double scaleY = displayBounds.getHeight() / contentBounds.getHeight();
|
||||
double scale = Math.min(scaleX, scaleY);
|
||||
|
||||
double scaledHeight = contentBounds.getHeight() * scale;
|
||||
|
||||
lastReflection = new AffineTransform();
|
||||
lastReflection.translate(0.0, contentBounds.getMinY() + contentBounds.getMaxY());
|
||||
lastReflection.scale(1.0, -1.0);
|
||||
|
||||
double scaledWidth = contentBounds.getWidth() * scale;
|
||||
|
||||
double offsetX = displayBounds.getMinX() + (displayBounds.getWidth() - scaledWidth) / 2.0;
|
||||
double offsetY = displayBounds.getMinY() + (displayBounds.getHeight() - scaledHeight) / 2.0;
|
||||
|
||||
double translateX = offsetX - contentBounds.getMinX() * scale;
|
||||
double translateY = offsetY - contentBounds.getMinY() * scale;
|
||||
|
||||
lastTransform = AffineTransform.getTranslateInstance(translateX, translateY);
|
||||
lastTransform.scale(scale, scale);
|
||||
|
||||
AffineTransform combined = new AffineTransform(lastTransform);
|
||||
combined.concatenate(lastReflection);
|
||||
|
||||
return combined;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package math;
|
||||
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public interface ViewTransformation
|
||||
{
|
||||
/**
|
||||
* @return The last reflection.
|
||||
*/
|
||||
public AffineTransform getLastReflection();
|
||||
|
||||
/**
|
||||
* @return The last transform.
|
||||
*/
|
||||
public AffineTransform getLastTransform();
|
||||
|
||||
/**
|
||||
* @param displayBounds
|
||||
* The bounds of the display area
|
||||
* @param contentBounds
|
||||
* The bounds of the content area
|
||||
* @return The transform that maps content coordinates to display coordinates, while maintaining
|
||||
* the aspect ratio of the content. The content is reflected across the horizontal axis to
|
||||
* match the typical display coordinate system where the y-axis increases downwards. The
|
||||
* transformation also centers the content within the display bounds.
|
||||
*/
|
||||
public AffineTransform getTransform(final Rectangle2D displayBounds,
|
||||
final Rectangle2D contentBounds);
|
||||
}
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../assets/usa48-counties.geo
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../assets/usa48-states.geo
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../assets/va-blocks.geo
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../assets/va-counties.geo
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../assets/va-streets-2024.geo
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../assets/world-countries.geo
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<fileset-config file-format-version="1.2.0" simple-config="true" sync-formatter="false">
|
||||
<fileset name="all" enabled="true" check-config-name="CS480StyleGuide" local="false">
|
||||
<file-match-pattern match-pattern="." include-pattern="true"/>
|
||||
</fileset>
|
||||
</fileset-config>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
|
||||
<classpathentry kind="src" path="src"/>
|
||||
<classpathentry kind="output" path="bin"/>
|
||||
</classpath>
|
||||
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>PA4</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>net.sf.eclipsecs.core.CheckstyleBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
<nature>net.sf.eclipsecs.core.CheckstyleNature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
||||
Binary file not shown.
Symlink
+1
@@ -0,0 +1 @@
|
||||
../assets/rockingham-streets-2024.geo
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../assets/rockingham-streets-2024.str
|
||||
@@ -0,0 +1,134 @@
|
||||
package app;
|
||||
|
||||
import geography.*;
|
||||
import gui.*;
|
||||
import java.awt.event.*;
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
import dataprocessing.Geocoder;
|
||||
import feature.*;
|
||||
|
||||
/**
|
||||
* The application for PA4.
|
||||
*
|
||||
* @author Prof. David Bernstein, James Madison University
|
||||
* @version 1.0
|
||||
*/
|
||||
public class PA4App implements ActionListener, Runnable, StreetSegmentObserver
|
||||
{
|
||||
private static final String EXIT = "Exit";
|
||||
private static final String SHOW = "Show";
|
||||
|
||||
private CartographyPanel<StreetSegment> panel;
|
||||
private CartographyDocument<StreetSegment> segments;
|
||||
private GeocodeDialog dialog;
|
||||
private JFrame frame;
|
||||
|
||||
/**
|
||||
* Handle actionPerformed() messages.
|
||||
*
|
||||
* @param evt The event that generated the message
|
||||
*/
|
||||
public void actionPerformed(final ActionEvent evt)
|
||||
{
|
||||
String ac = evt.getActionCommand();
|
||||
|
||||
if (ac.equals(SHOW))
|
||||
{
|
||||
if (!dialog.isVisible())
|
||||
{
|
||||
dialog.setLocation((int)frame.getBounds().getMaxX(), (int)frame.getBounds().getY());
|
||||
dialog.setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (ac.equals(EXIT))
|
||||
{
|
||||
dialog.dispose();
|
||||
frame.dispose();
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The code to be executed in the event dispatch thread.
|
||||
*/
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
InputStream isgeo = new FileInputStream(new File("rockingham-streets-2024.geo"));
|
||||
//InputStream isgeo = new FileInputStream(new File("va-streets-2024.geo"));
|
||||
AbstractMapProjection proj = new ConicalEqualAreaProjection(-96.0, 37.5, 29.5, 45.5);
|
||||
GeographicShapesReader gsReader = new GeographicShapesReader(isgeo, proj);
|
||||
CartographyDocument<GeographicShape> geographicShapes = gsReader.read();
|
||||
System.out.println("Read the .geo file");
|
||||
|
||||
InputStream iss = new FileInputStream(new File("rockingham-streets-2024.str"));
|
||||
//InputStream iss = new FileInputStream(new File("va-streets-2024.str"));
|
||||
StreetsReader sReader = new StreetsReader(iss, geographicShapes);
|
||||
Map<String, Street> streets = new HashMap<String, Street>();
|
||||
segments = sReader.read(streets);
|
||||
System.out.println("Read the .str file");
|
||||
|
||||
panel = new CartographyPanel<StreetSegment>(segments, new StreetSegmentCartographer());
|
||||
frame = new JFrame("Map");
|
||||
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
frame.setSize(600, 600);
|
||||
|
||||
JMenuBar menuBar = new JMenuBar();
|
||||
frame.setJMenuBar(menuBar);
|
||||
JMenuItem item;
|
||||
JMenu menu;
|
||||
|
||||
menu = new JMenu("File");
|
||||
menuBar.add(menu);
|
||||
|
||||
item = new JMenuItem(EXIT);
|
||||
item.addActionListener(this);
|
||||
menu.add(item);
|
||||
|
||||
menu = new JMenu("Geocoder");
|
||||
menuBar.add(menu);
|
||||
|
||||
item = new JMenuItem(SHOW);
|
||||
item.addActionListener(this);
|
||||
menu.add(item);
|
||||
|
||||
frame.setContentPane(panel);
|
||||
frame.setVisible(true);
|
||||
|
||||
Geocoder geocoder = new Geocoder(geographicShapes, segments, streets);
|
||||
dialog = new GeocodeDialog(frame, geocoder);
|
||||
dialog.addStreetSegmentObserver(this);
|
||||
dialog.setLocation((int)frame.getBounds().getMaxX(), (int)frame.getBounds().getY());
|
||||
dialog.setVisible(true);
|
||||
}
|
||||
catch (IOException ioe)
|
||||
{
|
||||
JOptionPane.showMessageDialog(frame,
|
||||
ioe.toString(),
|
||||
"Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a collection of StreetSegment objects.
|
||||
*
|
||||
* @param segmentIDs The IDs of the StreetSegment objects
|
||||
*/
|
||||
@Override
|
||||
public void handleStreetSegments(final List<String> segmentIDs)
|
||||
{
|
||||
HashMap<String, StreetSegment> highlighted = new HashMap<String, StreetSegment>();
|
||||
for (String id: segmentIDs) highlighted.put(id, segments.getElement(id));
|
||||
segments.setHighlighted(highlighted);
|
||||
panel.repaint();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package app;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import javax.swing.*;
|
||||
|
||||
/**
|
||||
* The main class for PA4.
|
||||
*
|
||||
* @author Prof. David Bernstein, James Madison University
|
||||
* @version 1.0
|
||||
*/
|
||||
public class PA4Driver {
|
||||
|
||||
/**
|
||||
* The entry point for the application.
|
||||
*
|
||||
* @param args The command-line arguments (IGNORED)
|
||||
* @throws InterruptedException if something goes wrong with Swing
|
||||
* @throws InvocationTargetException if something goes wrong with Swing
|
||||
*/
|
||||
public static void main(final String[] args)
|
||||
throws InterruptedException, InvocationTargetException
|
||||
{
|
||||
SwingUtilities.invokeAndWait(new PA4App());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package dataprocessing;
|
||||
|
||||
import java.awt.Shape;
|
||||
import java.awt.geom.PathIterator;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import feature.Street;
|
||||
import feature.StreetSegment;
|
||||
import geography.GeographicShape;
|
||||
import gui.CartographyDocument;
|
||||
|
||||
/**
|
||||
* Maps a street name and number to coordinates along matching street segments.
|
||||
*/
|
||||
public class Geocoder
|
||||
{
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private final CartographyDocument<GeographicShape> shapes;
|
||||
@SuppressWarnings("unused")
|
||||
private final CartographyDocument<StreetSegment> segments;
|
||||
private final Map<String, Street> streets;
|
||||
|
||||
/**
|
||||
* @param shapes
|
||||
* @param segments
|
||||
* @param streets
|
||||
*/
|
||||
public Geocoder(final CartographyDocument<GeographicShape> shapes,
|
||||
final CartographyDocument<StreetSegment> segments, final Map<String, Street> streets)
|
||||
{
|
||||
this.shapes = shapes;
|
||||
this.segments = segments;
|
||||
this.streets = streets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns interpolated coordinates for the given address. Matching segment IDs are added to
|
||||
* {@code segmentIDs}.
|
||||
*
|
||||
* @param canonicalName
|
||||
* street name
|
||||
* @param streetNumber
|
||||
* address number
|
||||
* @param segmentIDs
|
||||
* output list populated with matching segment IDs
|
||||
* @return list of coordinates
|
||||
*/
|
||||
public List<double[]> geocode(final String canonicalName, final int streetNumber,
|
||||
final List<String> segmentIDs)
|
||||
{
|
||||
List<double[]> locations = new ArrayList<>();
|
||||
Street street = streets.get(canonicalName);
|
||||
|
||||
if (street == null)
|
||||
{
|
||||
return locations;
|
||||
}
|
||||
|
||||
for (StreetSegment segment : street.getSegments(streetNumber))
|
||||
{
|
||||
segmentIDs.add(segment.getID());
|
||||
locations.add(interpolateAddress(segment, streetNumber));
|
||||
}
|
||||
|
||||
return locations;
|
||||
}
|
||||
|
||||
private double[] interpolateAddress(final StreetSegment segment, final int streetNumber)
|
||||
{
|
||||
int low = segment.getLowAddress();
|
||||
int high = segment.getHighAddress();
|
||||
double ratio = (streetNumber - low) / (double) (high - low);
|
||||
|
||||
List<double[]> points = extractPoints(segment.getGeographicShape().getShape());
|
||||
double totalLength = lineLength(points);
|
||||
double targetLength = ratio * totalLength;
|
||||
|
||||
return walkToTarget(points, targetLength);
|
||||
}
|
||||
|
||||
private List<double[]> extractPoints(final Shape shape)
|
||||
{
|
||||
List<double[]> points = new ArrayList<>();
|
||||
double[] coords = new double[6];
|
||||
PathIterator it = shape.getPathIterator(null);
|
||||
|
||||
while (!it.isDone())
|
||||
{
|
||||
int type = it.currentSegment(coords);
|
||||
if (type != PathIterator.SEG_CLOSE)
|
||||
{
|
||||
points.add(new double[] {coords[0], coords[1]});
|
||||
}
|
||||
it.next();
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
private double lineLength(final List<double[]> points)
|
||||
{
|
||||
double total = 0.0;
|
||||
for (int i = 1; i < points.size(); i++)
|
||||
{
|
||||
total += distance(points.get(i - 1), points.get(i));
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
private double[] walkToTarget(final List<double[]> points, final double targetLength)
|
||||
{
|
||||
double travelled = 0.0;
|
||||
|
||||
for (int i = 1; i < points.size(); i++)
|
||||
{
|
||||
double[] start = points.get(i - 1);
|
||||
double[] end = points.get(i);
|
||||
double segLen = distance(start, end);
|
||||
|
||||
if (travelled + segLen >= targetLength)
|
||||
{
|
||||
double t = (targetLength - travelled) / segLen;
|
||||
return new double[] {start[0] + t * (end[0] - start[0]),
|
||||
start[1] + t * (end[1] - start[1])};
|
||||
}
|
||||
|
||||
travelled += segLen;
|
||||
}
|
||||
|
||||
return points.get(points.size() - 1);
|
||||
}
|
||||
|
||||
private double distance(final double[] a, final double[] b)
|
||||
{
|
||||
double dx = b[0] - a[0];
|
||||
double dy = b[1] - a[1];
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package feature;
|
||||
|
||||
import geography.GeographicShape;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public abstract class AbstractFeature implements Feature
|
||||
{
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* @param id
|
||||
*/
|
||||
public AbstractFeature(final String id)
|
||||
{
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getID()
|
||||
{
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public abstract GeographicShape getGeographicShape();
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package feature;
|
||||
|
||||
import geography.GeographicShape;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public interface Feature
|
||||
{
|
||||
/**
|
||||
* Get the ID of this feature.
|
||||
*
|
||||
* @return The ID
|
||||
*/
|
||||
public String getID();
|
||||
|
||||
|
||||
/**
|
||||
* @return The geographic shape of this feature
|
||||
*/
|
||||
public GeographicShape getGeographicShape();
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package feature;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A class representing an intersection of streets. Each intersection has a list of inbound and
|
||||
* outbound StreetSegments.
|
||||
*/
|
||||
public class Intersection
|
||||
{
|
||||
private List<StreetSegment> inbound;
|
||||
private List<StreetSegment> outbound;
|
||||
|
||||
/**
|
||||
* Constructor. Initializes the inbound and outbound lists to empty lists.
|
||||
*/
|
||||
public Intersection()
|
||||
{
|
||||
this.inbound = new ArrayList<>();
|
||||
this.outbound = new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param segment The StreetSegment to add to the list of inbound segments.
|
||||
*/
|
||||
public void addInbound(final StreetSegment segment)
|
||||
{
|
||||
inbound.add(segment);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param segment The StreetSegment to add to the list of outbound segments.
|
||||
*/
|
||||
public void addOutbound(final StreetSegment segment)
|
||||
{
|
||||
outbound.add(segment);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The list of inbound StreetSegments.
|
||||
*/
|
||||
public List<StreetSegment> getInbound()
|
||||
{
|
||||
return inbound;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The list of outbound StreetSegments.
|
||||
*/
|
||||
public List<StreetSegment> getOutbound()
|
||||
{
|
||||
return outbound;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package feature;
|
||||
|
||||
import geography.GeographicShape;
|
||||
import geography.PiecewiseLinearCurve;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents a street containing one or more segments.
|
||||
*/
|
||||
public class Street extends AbstractFeature
|
||||
{
|
||||
private static final String SPACE = " ";
|
||||
|
||||
private PiecewiseLinearCurve shape;
|
||||
@SuppressWarnings("unused")
|
||||
private String category;
|
||||
@SuppressWarnings("unused")
|
||||
private String code;
|
||||
@SuppressWarnings("unused")
|
||||
private String name;
|
||||
@SuppressWarnings("unused")
|
||||
private String prefix;
|
||||
@SuppressWarnings("unused")
|
||||
private String suffix;
|
||||
private List<StreetSegment> segments;
|
||||
|
||||
|
||||
/**
|
||||
* @param prefix
|
||||
* @param name
|
||||
* @param category
|
||||
* @param suffix
|
||||
* @param code
|
||||
*/
|
||||
public Street(final String prefix, final String name, final String category, final String suffix,
|
||||
final String code)
|
||||
{
|
||||
super(code);
|
||||
this.shape = new PiecewiseLinearCurve(code);
|
||||
this.prefix = prefix;
|
||||
this.name = name;
|
||||
this.category = category;
|
||||
this.suffix = suffix;
|
||||
this.code = code;
|
||||
this.segments = new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param segment
|
||||
*/
|
||||
public void addSegment(final StreetSegment segment)
|
||||
{
|
||||
segments.add(segment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a canonical name from street components.
|
||||
*
|
||||
* @param prefix
|
||||
* The street prefix (e.g., "N", "E")
|
||||
* @param name
|
||||
* The street name
|
||||
* @param category
|
||||
* The street category (e.g., "St", "Ave")
|
||||
* @param suffix
|
||||
* The street suffix (e.g., "NW", "SE")
|
||||
* @return The canonical street name
|
||||
*/
|
||||
public static String createCanonicalName(final String prefix, final String name,
|
||||
final String category, final String suffix)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
if (prefix != null)
|
||||
{
|
||||
sb.append(prefix).append(SPACE);
|
||||
}
|
||||
|
||||
if (name != null)
|
||||
{
|
||||
sb.append(name).append(SPACE);
|
||||
}
|
||||
|
||||
if (category != null)
|
||||
{
|
||||
sb.append(category).append(SPACE);
|
||||
}
|
||||
|
||||
if (suffix != null)
|
||||
{
|
||||
sb.append(suffix);
|
||||
}
|
||||
|
||||
return sb.toString().trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the segments of this street.
|
||||
*
|
||||
* @param number
|
||||
* @return The list of segments
|
||||
*/
|
||||
public List<StreetSegment> getSegments(final int number)
|
||||
{
|
||||
List<StreetSegment> numSegments = new ArrayList<>();
|
||||
for (StreetSegment segment : this.segments)
|
||||
{
|
||||
if (segment.getLowAddress() <= number && segment.getHighAddress() >= number)
|
||||
{
|
||||
segments.add(segment);
|
||||
}
|
||||
}
|
||||
|
||||
return numSegments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The segments
|
||||
*/
|
||||
public Iterator<StreetSegment> getSegments()
|
||||
{
|
||||
return segments.iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public GeographicShape getGeographicShape()
|
||||
{
|
||||
return shape;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The size
|
||||
*/
|
||||
public int getSize()
|
||||
{
|
||||
return segments.size();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package feature;
|
||||
|
||||
import geography.GeographicShape;
|
||||
|
||||
/**
|
||||
* Represents a street segment (a portion of a street between two points).
|
||||
*/
|
||||
public class StreetSegment extends AbstractFeature
|
||||
{
|
||||
private double length;
|
||||
private int tail;
|
||||
private int head;
|
||||
private int highAddress;
|
||||
private int lowAddress;
|
||||
private GeographicShape shape;
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* @param id
|
||||
* @param code
|
||||
* @param shape
|
||||
* @param lowAddress
|
||||
* @param highAddress
|
||||
* @param tail
|
||||
* @param head
|
||||
* @param length
|
||||
*/
|
||||
public StreetSegment(final String id, final String code, final GeographicShape shape,
|
||||
final int lowAddress, final int highAddress, final int tail, final int head,
|
||||
final double length)
|
||||
{
|
||||
super(id);
|
||||
this.length = length;
|
||||
this.tail = tail;
|
||||
this.head = head;
|
||||
this.highAddress = highAddress;
|
||||
this.lowAddress = lowAddress;
|
||||
this.shape = shape;
|
||||
this.code = (code != null && code.length() >= 2) ? code.substring(0, 2) : code;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The length of this street segment
|
||||
*/
|
||||
public double getLength()
|
||||
{
|
||||
return length;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The tail of this street segment
|
||||
*/
|
||||
public int getTail()
|
||||
{
|
||||
return tail;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The head of this street segment
|
||||
*/
|
||||
public int getHead()
|
||||
{
|
||||
return head;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The high address of this street segment
|
||||
*/
|
||||
public int getHighAddress()
|
||||
{
|
||||
return highAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The low address of this street segment
|
||||
*/
|
||||
public int getLowAddress()
|
||||
{
|
||||
return lowAddress;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GeographicShape getGeographicShape()
|
||||
{
|
||||
return shape;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The code of this street segment
|
||||
*/
|
||||
public String getCode()
|
||||
{
|
||||
return code;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package feature;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* An observer of events related to StreetSegment objects.
|
||||
*
|
||||
* @author Prof. David Bernstein, James Madison University
|
||||
* @version 1.0
|
||||
*/
|
||||
public interface StreetSegmentObserver
|
||||
{
|
||||
/**
|
||||
* Respond to handleStreetSegments() messages.
|
||||
*
|
||||
* @param segmentIDs The IDs of the StreetSegment objects
|
||||
*/
|
||||
public abstract void handleStreetSegments(List<String> segmentIDs);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package feature;
|
||||
|
||||
/**
|
||||
* An generator of events related to StreetSegment objects.
|
||||
*
|
||||
* @author Prof. David Bernstein, James Madison University
|
||||
* @version 1.0
|
||||
*/
|
||||
public interface StreetSegmentSubject
|
||||
{
|
||||
/**
|
||||
* Add a StreetSegmentObserver.
|
||||
*
|
||||
* @param observer The observer
|
||||
*/
|
||||
public abstract void addStreetSegmentObserver(final StreetSegmentObserver observer);
|
||||
|
||||
/**
|
||||
* Remove a StreetSegmentObserver.
|
||||
*
|
||||
* @param observer The observer
|
||||
*/
|
||||
public abstract void removeStreetSegmentObserver(final StreetSegmentObserver observer);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package feature;
|
||||
import geography.*;
|
||||
|
||||
import java.awt.*;
|
||||
import java.util.*;
|
||||
|
||||
|
||||
/**
|
||||
* A ThemeLibrary for use with Street objects.
|
||||
*
|
||||
* @author Prof. David Bernstein, James Madison University
|
||||
* @version 1
|
||||
*/
|
||||
public class StreetThemeLibrary implements ThemeLibrary
|
||||
{
|
||||
private static final Theme DEFAULT_THEME = new Theme(Color.BLACK, new BasicStroke());
|
||||
|
||||
private Theme highlightTheme;
|
||||
private Map<String, Theme> themes;
|
||||
|
||||
/**
|
||||
* Default Constructor.
|
||||
*/
|
||||
public StreetThemeLibrary()
|
||||
{
|
||||
themes = new HashMap<String, Theme>();
|
||||
|
||||
// Interstate, U.S., and other highways
|
||||
themes.put("A1", new Theme(new Color( 0, 153, 204), new BasicStroke(3.0f)));
|
||||
themes.put("A2", new Theme(new Color(102, 153, 204), new BasicStroke(1.0f)));
|
||||
|
||||
// Secondary and connecting roads
|
||||
themes.put("A3", new Theme(new Color(102, 153, 102), new BasicStroke(3.0f)));
|
||||
|
||||
// Local roads and special purpose roads
|
||||
themes.put("A4", new Theme(new Color(153, 153, 153), new BasicStroke(1.0f)));
|
||||
themes.put("A6", new Theme(new Color(153, 153, 153), new BasicStroke(1.0f)));
|
||||
|
||||
// Dirt roads, alleys, and unclassified roads
|
||||
float[] pattern = new float[2];
|
||||
pattern[0] = 10.0f;
|
||||
pattern[1] = 10.0f;
|
||||
themes.put("A5", new Theme(new Color(211,211,211),
|
||||
new BasicStroke(1.0f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER,
|
||||
10.0f, pattern, 0.0f)));
|
||||
themes.put("A7", new Theme(new Color(211,211,211),
|
||||
new BasicStroke(1.0f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER,
|
||||
10.0f, pattern, 0.0f)));
|
||||
|
||||
// Highlighted elements
|
||||
highlightTheme = new Theme(new Color( 0, 255, 0, 128), new BasicStroke(5.0f));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Theme to use for highlighted elements.
|
||||
*
|
||||
* @return The Theme
|
||||
*/
|
||||
@Override
|
||||
public Theme getHighlightTheme()
|
||||
{
|
||||
return highlightTheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Theme to use for the given code.
|
||||
*
|
||||
* @param code The code
|
||||
* @return The Theme
|
||||
*/
|
||||
@Override
|
||||
public Theme getTheme(final String code)
|
||||
{
|
||||
Theme result = themes.get(code);
|
||||
if (result == null) result = DEFAULT_THEME;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package feature;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import geography.GeographicShape;
|
||||
import gui.CartographyDocument;
|
||||
|
||||
/**
|
||||
* Reader for street data files (.str files).
|
||||
*/
|
||||
public class StreetsReader
|
||||
{
|
||||
private BufferedReader in;
|
||||
private CartographyDocument<GeographicShape> geographicShapes;
|
||||
|
||||
/**
|
||||
* Explicit Value Constructor.
|
||||
*
|
||||
* @param is
|
||||
* The input stream for the .str file
|
||||
* @param shapes
|
||||
* The geographic shapes document
|
||||
*/
|
||||
public StreetsReader(final InputStream is, final CartographyDocument<GeographicShape> shapes)
|
||||
{
|
||||
this.in = new BufferedReader(new InputStreamReader(is));
|
||||
this.geographicShapes = shapes;
|
||||
}
|
||||
|
||||
private static String safeGet(final String[] arr, final int index)
|
||||
{
|
||||
if (arr == null || index < 0 || index >= arr.length)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
String value = arr[index];
|
||||
return (value == null || value.isBlank()) ? null : value.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the streets.
|
||||
*
|
||||
* @param streets
|
||||
* The map to store streets
|
||||
* @return The cartography document containing street segments
|
||||
*/
|
||||
public CartographyDocument<StreetSegment> read(final Map<String, Street> streets)
|
||||
throws IOException
|
||||
{
|
||||
Map<String, StreetSegment> segments = new HashMap<String, StreetSegment>();
|
||||
String currentLine;
|
||||
while ((currentLine = in.readLine()) != null)
|
||||
{
|
||||
String[] values = currentLine.trim().split("\t");
|
||||
int tail = Integer.parseInt(values[0].trim());
|
||||
int head = Integer.parseInt(values[1].trim());
|
||||
double length = Double.parseDouble(values[2].trim());
|
||||
String code = values[3].trim();
|
||||
String id = values[4].trim();
|
||||
String prefix = safeGet(values, 5);
|
||||
String name = safeGet(values, 6);
|
||||
String category = safeGet(values, 7);
|
||||
String suffix = safeGet(values, 8);
|
||||
int lowAddress = Integer.MIN_VALUE;
|
||||
int highAddress = Integer.MIN_VALUE;
|
||||
|
||||
if (safeGet(values, 9) != null && safeGet(values, 10) != null)
|
||||
{
|
||||
int tailAddress = Integer.parseInt(values[9].trim());
|
||||
int headAddress = Integer.parseInt(values[10].trim());
|
||||
|
||||
lowAddress = Math.min(tailAddress, headAddress);
|
||||
highAddress = Math.max(tailAddress, headAddress);
|
||||
}
|
||||
|
||||
StreetSegment segment = new StreetSegment(id, code, geographicShapes.getElement(id),
|
||||
lowAddress, highAddress, tail, head, length);
|
||||
|
||||
String canonicalName = Street.createCanonicalName(prefix, name, category, suffix);
|
||||
Street street = streets.get(canonicalName);
|
||||
if (street == null)
|
||||
{
|
||||
street = new Street(prefix, name, category, suffix, code);
|
||||
streets.put(canonicalName, street);
|
||||
}
|
||||
|
||||
street.addSegment(segment);
|
||||
segments.put(id, segment);
|
||||
}
|
||||
|
||||
return new CartographyDocument<StreetSegment>(segments, geographicShapes.getBounds());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package geography;
|
||||
|
||||
import java.awt.Shape;
|
||||
|
||||
/**
|
||||
* A GeographicShape is a shape that can be associated with a geographic location.
|
||||
*/
|
||||
public abstract class AbstractGeographicShape implements GeographicShape
|
||||
{
|
||||
private String id;
|
||||
|
||||
|
||||
/**
|
||||
* @param id The ID of the shape.
|
||||
*/
|
||||
public AbstractGeographicShape(final String id)
|
||||
{
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getID()
|
||||
{
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public abstract Shape getShape();
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package geography;
|
||||
/**
|
||||
* An abstract class that is extended to construct different types
|
||||
* map projections.
|
||||
*
|
||||
* @author Prof. David Bernstein, James Madison University
|
||||
* @version 1.0
|
||||
*/
|
||||
public abstract class AbstractMapProjection implements MapProjection
|
||||
{
|
||||
// Radius of spherical Earth (in km)
|
||||
protected static final double R = 6369.0;
|
||||
|
||||
protected static final double PI = Math.PI;
|
||||
protected static final double TWO_PI = 2.0*Math.PI;
|
||||
protected static final double PI_OVER_TWO = Math.PI/2.0;
|
||||
protected static final double PI_OVER_FOUR = Math.PI/4.0;
|
||||
|
||||
protected static final double RADIANS_PER_DEGREE = 2.0*Math.PI/360.0;
|
||||
|
||||
protected static final double DEGREES_PER_RADIAN = 1.0 / RADIANS_PER_DEGREE;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* The forward transformation (i.e., from Longitude/Latitude in
|
||||
* __degrees__ to kilometers above the equator and to the west of the
|
||||
* reference meridian).
|
||||
*
|
||||
* @param ll The longitude and latitude in __degrees__ (in that order)
|
||||
* @return KMs west of reference and north of equator (in that order)
|
||||
*/
|
||||
@Override
|
||||
public double[] forward(final double[] ll)
|
||||
{
|
||||
double lambda = ll[0] * RADIANS_PER_DEGREE;
|
||||
double phi = ll[1] * RADIANS_PER_DEGREE;
|
||||
|
||||
return forward(lambda, phi);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* The forward transformation (i.e., from Longitude/Latitude in
|
||||
* __radians__ to kilometers above the equator and to the west of the
|
||||
* reference meridian).
|
||||
*
|
||||
* @param lambda The longitude in __radians__
|
||||
* @param phi The latitude in __radians__
|
||||
* @return KMs west of reference and north of equator (in that order)
|
||||
*/
|
||||
@Override
|
||||
public abstract double[] forward(final double lambda, final double phi);
|
||||
|
||||
|
||||
/**
|
||||
* The inverse transformation from kilometers (above the
|
||||
* equator and to the west of the reference meridian)
|
||||
* to Longitude/Latitude in __degrees__.
|
||||
*
|
||||
* @param km The point in above,west coordinates (in that order)
|
||||
* @return The longitude and latitude in __degrees__ (in that order)
|
||||
*/
|
||||
@Override
|
||||
public double[] inverse(final double[] km)
|
||||
{
|
||||
double[] ll = inverse(km[0], km[1]);// In radians
|
||||
ll[0] = ll[0] * DEGREES_PER_RADIAN; // In degrees
|
||||
ll[1] = ll[1] * DEGREES_PER_RADIAN; // In degrees
|
||||
|
||||
return ll;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The inverse transformation from kilometers (above the
|
||||
* equator and to the west of the reference meridian)
|
||||
* to Longitude/Latitude in __radians__.
|
||||
*
|
||||
* @param ew The distance east/west in kilometers
|
||||
* @param ns The distance north/south in kilometers
|
||||
* @return The longitude and latitude in __radians__ (in that order)
|
||||
*/
|
||||
@Override
|
||||
public abstract double[] inverse(final double ew, final double ns);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package geography;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class ConicalEqualAreaProjection extends AbstractMapProjection
|
||||
{
|
||||
private double lamda0;
|
||||
private double phi0;
|
||||
private double phi1;
|
||||
private double phi2;
|
||||
|
||||
private double n;
|
||||
private double c;
|
||||
private double rho0;
|
||||
|
||||
/**
|
||||
* @param refM The reference meridian (in degrees)
|
||||
* @param refP The reference parallel (in degrees)
|
||||
* @param refP1 The first standard parallel (in degrees)
|
||||
* @param refP2 The second standard parallel (in degrees)
|
||||
*/
|
||||
public ConicalEqualAreaProjection(final double refM, final double refP, final double refP1,
|
||||
final double refP2)
|
||||
{
|
||||
this.lamda0 = refM * RADIANS_PER_DEGREE;
|
||||
this.phi0 = refP * RADIANS_PER_DEGREE;
|
||||
this.phi1 = refP1 * RADIANS_PER_DEGREE;
|
||||
this.phi2 = refP2 * RADIANS_PER_DEGREE;
|
||||
|
||||
n = 0.5 * (Math.sin(phi1) + Math.sin(phi2));
|
||||
c = Math.cos(phi1) * Math.cos(phi1) + 2.0 * n * Math.sin(phi1);
|
||||
rho0 = Math.sqrt(c - 2.0 * n * Math.sin(phi0)) / n;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double[] forward(final double lambda, final double phi)
|
||||
{
|
||||
double rho = Math.sqrt(c - 2.0 * n * Math.sin(phi)) / n;
|
||||
double theta = n * (lambda - lamda0);
|
||||
|
||||
return new double[] {R * rho * Math.sin(theta), R * (rho0 - rho * Math.cos(theta))};
|
||||
}
|
||||
|
||||
@Override
|
||||
public double[] inverse(final double ew, final double ns)
|
||||
{
|
||||
double a = Math.sqrt(Math.pow((ew / R), 2) + Math.pow((rho0 - ns / R), 2));
|
||||
double b = Math.atan((ew / R) / (rho0 - ns / R));
|
||||
|
||||
return new double[] {lamda0 + b / n, Math.asin((c - Math.pow(a * n, 2)) / (2.0 * n))};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package geography;
|
||||
|
||||
import java.awt.Shape;
|
||||
|
||||
/**
|
||||
* A GeographicShape is a shape that can be associated with a geographic location.
|
||||
*/
|
||||
public interface GeographicShape
|
||||
{
|
||||
/**
|
||||
* @return The ID of the shape.
|
||||
*/
|
||||
public String getID();
|
||||
|
||||
/**
|
||||
* @return The shape.
|
||||
*/
|
||||
public Shape getShape();
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package geography;
|
||||
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import gui.CartographyDocument;
|
||||
|
||||
/**
|
||||
* A class for reading geographic shapes from an input stream and creating a CartographyDocument
|
||||
* containing those shapes.
|
||||
*/
|
||||
public class GeographicShapesReader
|
||||
{
|
||||
private BufferedReader in;
|
||||
private MapProjection proj;
|
||||
|
||||
/**
|
||||
* @param is
|
||||
* The input stream to read the shapes from
|
||||
* @param proj
|
||||
* The map projection to use when converting geographic coordinates to Cartesian
|
||||
* coordinates
|
||||
*/
|
||||
public GeographicShapesReader(final InputStream is, final MapProjection proj)
|
||||
{
|
||||
this.in = new BufferedReader(new InputStreamReader(is));
|
||||
this.proj = proj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A CartographyDocument containing the shapes read from the input stream.
|
||||
*/
|
||||
public CartographyDocument<GeographicShape> read()
|
||||
{
|
||||
double maxX = Double.NEGATIVE_INFINITY;
|
||||
double maxY = Double.NEGATIVE_INFINITY;
|
||||
double minX = Double.POSITIVE_INFINITY;
|
||||
double minY = Double.POSITIVE_INFINITY;
|
||||
|
||||
Map<String, GeographicShape> shapes = new HashMap<>();
|
||||
|
||||
String currentLine;
|
||||
try
|
||||
{
|
||||
while ((currentLine = in.readLine()) != null)
|
||||
{
|
||||
Map<String, String> parsed = new LinkedHashMap<>();
|
||||
String[] tokens = currentLine.trim().split("(?=Type:|ID:|Code:)");
|
||||
|
||||
for (String token : tokens)
|
||||
{
|
||||
if (token.isBlank())
|
||||
continue;
|
||||
int colonIdx = token.indexOf(':');
|
||||
String key = token.substring(0, colonIdx).trim();
|
||||
String value = token.substring(colonIdx + 1).trim();
|
||||
parsed.put(key, value);
|
||||
}
|
||||
|
||||
String type = parsed.get("Type");
|
||||
String id = parsed.get("ID");
|
||||
|
||||
PiecewiseLinearCurve shape;
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case "PiecewiseLinearCurve":
|
||||
shape = new PiecewiseLinearCurve(id);
|
||||
break;
|
||||
case "Polygon":
|
||||
shape = new Polygon(id);
|
||||
break;
|
||||
case "Comment":
|
||||
in.readLine(); // skip the END
|
||||
continue;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
while ((currentLine = in.readLine()) != null)
|
||||
{
|
||||
currentLine = currentLine.trim();
|
||||
|
||||
if (currentLine.equals("END"))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
String[] strCoords = currentLine.trim().split("\\s+");
|
||||
double[] coords = new double[] {Double.parseDouble(strCoords[0]),
|
||||
Double.parseDouble(strCoords[1])};
|
||||
|
||||
double[] projCoords = proj.forward(coords);
|
||||
shape.add(projCoords);
|
||||
|
||||
maxX = Math.max(maxX, projCoords[0]);
|
||||
maxY = Math.max(maxY, projCoords[1]);
|
||||
minX = Math.min(minX, projCoords[0]);
|
||||
minY = Math.min(minY, projCoords[1]);
|
||||
}
|
||||
shapes.put(id, shape);
|
||||
}
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
Rectangle2D.Double bounds = new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY);
|
||||
return new CartographyDocument<>(shapes, bounds);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package geography;
|
||||
|
||||
/**
|
||||
* The requirements of a map projection.
|
||||
*
|
||||
* @author Prof. David Bernstein, James Madison University
|
||||
* @version 1.0
|
||||
*/
|
||||
public interface MapProjection
|
||||
{
|
||||
|
||||
/**
|
||||
* The forward transformation (i.e., from Longitude/Latitude in
|
||||
* __degrees__ to kilometers above the equator and to the west of the
|
||||
* reference meridian).
|
||||
*
|
||||
* @param ll The longitude and latitude in __degrees__ (in that order)
|
||||
* @return KMs west of reference and north of equator (in that order)
|
||||
*/
|
||||
public abstract double[] forward(double[] ll);
|
||||
|
||||
|
||||
/**
|
||||
* The forward transformation (i.e., from Longitude/Latitude in
|
||||
* __radians__ to kilometers above the equator and to the west of the
|
||||
* reference meridian).
|
||||
*
|
||||
* @param lambda The longitude in __radians__
|
||||
* @param phi The latitude in __radians__
|
||||
* @return KMs west of reference and north of equator (in that order)
|
||||
*/
|
||||
public abstract double[] forward(double lambda, double phi);
|
||||
|
||||
|
||||
/**
|
||||
* The inverse transformation from kilometers (above the
|
||||
* equator and to the west of the reference meridian)
|
||||
* to Longitude/Latitude in __degrees__.
|
||||
*
|
||||
* @param km The point in above,west coordinates (in that order)
|
||||
* @return The longitude and latitude in __degrees__ (in that order)
|
||||
*/
|
||||
public abstract double[] inverse(double[] km);
|
||||
|
||||
/**
|
||||
* The inverse transformation from kilometers (above the
|
||||
* equator and to the west of the reference meridian)
|
||||
* to Longitude/Latitude in __radians__.
|
||||
*
|
||||
* @param ew The distance east/west in kilometers
|
||||
* @param ns The distance north/south in kilometers
|
||||
* @return The longitude and latitude in __radians__ (in that order)
|
||||
*/
|
||||
public abstract double[] inverse(double ew, double ns);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package geography;
|
||||
|
||||
import java.awt.Shape;
|
||||
import java.awt.geom.Path2D;
|
||||
|
||||
/**
|
||||
* A class representing a piecewise linear curve.
|
||||
*/
|
||||
public class PiecewiseLinearCurve extends AbstractGeographicShape
|
||||
{
|
||||
protected Path2D.Double shape;
|
||||
|
||||
/**
|
||||
* @param id
|
||||
* The id of the curve
|
||||
*/
|
||||
public PiecewiseLinearCurve(final String id)
|
||||
{
|
||||
super(id);
|
||||
shape = new Path2D.Double();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id
|
||||
* The id of the curve
|
||||
* @param path
|
||||
* The path of the curve
|
||||
*/
|
||||
public PiecewiseLinearCurve(final String id, final Path2D.Double path)
|
||||
{
|
||||
super(id);
|
||||
this.shape = path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Shape getShape()
|
||||
{
|
||||
return shape;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param point
|
||||
* The point to add to the curve
|
||||
*/
|
||||
public void add(final double[] point)
|
||||
{
|
||||
if (shape.getCurrentPoint() == null)
|
||||
{
|
||||
shape.moveTo(point[0], point[1]);
|
||||
}
|
||||
else
|
||||
{
|
||||
shape.lineTo(point[0], point[1]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param addition
|
||||
* The shape to add to the curve
|
||||
* @param connect
|
||||
* Whether to connect the current curve to the addition with a line segment (if true, the
|
||||
* addition will be connected to the current curve with a line segment; if false, the
|
||||
* addition will be added as a separate shape)
|
||||
*/
|
||||
public void append(final Shape addition, final boolean connect)
|
||||
{
|
||||
if (connect && shape.getCurrentPoint() != null)
|
||||
{
|
||||
shape.lineTo(addition.getBounds2D().getX(), addition.getBounds2D().getY());
|
||||
}
|
||||
shape.append(addition, false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package geography;
|
||||
|
||||
import java.awt.Shape;
|
||||
import java.awt.geom.Path2D;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class Polygon extends PiecewiseLinearCurve
|
||||
{
|
||||
/**
|
||||
* @param id The id of the polygon
|
||||
*/
|
||||
public Polygon(final String id)
|
||||
{
|
||||
super(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id The id of the polygon
|
||||
* @param path The path of the polygon
|
||||
*/
|
||||
public Polygon(final String id, final Path2D.Double path)
|
||||
{
|
||||
super(id, path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Shape getShape()
|
||||
{
|
||||
shape.closePath();
|
||||
return shape;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package geography;
|
||||
|
||||
/**
|
||||
* A map projection that maps the globe onto a plane using the sinusoidal projection.
|
||||
*/
|
||||
public class SinusoidalProjection extends AbstractMapProjection
|
||||
{
|
||||
|
||||
@Override
|
||||
public double[] forward(final double lambda, final double phi)
|
||||
{
|
||||
return new double[] {R * lambda * Math.cos(phi), R * phi};
|
||||
}
|
||||
|
||||
@Override
|
||||
public double[] inverse(final double ew, final double ns)
|
||||
{
|
||||
double phi = ns / R;
|
||||
return new double[] {ew / (R * Math.cos(phi)), phi};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package geography;
|
||||
|
||||
import java.awt.BasicStroke;
|
||||
import java.awt.Color;
|
||||
|
||||
/**
|
||||
* A theme for rendering geographic elements.
|
||||
*/
|
||||
public class Theme
|
||||
{
|
||||
private Color color;
|
||||
private BasicStroke stroke;
|
||||
|
||||
/**
|
||||
* Explicit Value Constructor.
|
||||
*
|
||||
* @param color
|
||||
* The color
|
||||
* @param stroke
|
||||
* The stroke
|
||||
*/
|
||||
public Theme(final Color color, final BasicStroke stroke)
|
||||
{
|
||||
this.color = color;
|
||||
this.stroke = stroke;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the color.
|
||||
*
|
||||
* @return The color
|
||||
*/
|
||||
public Color getColor()
|
||||
{
|
||||
return color;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the stroke.
|
||||
*
|
||||
* @return The stroke
|
||||
*/
|
||||
public BasicStroke getStroke()
|
||||
{
|
||||
return stroke;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package geography;
|
||||
|
||||
/**
|
||||
* Interface for theme libraries that provide rendering themes.
|
||||
*/
|
||||
public interface ThemeLibrary
|
||||
{
|
||||
/**
|
||||
* Get the theme for highlighted elements.
|
||||
*
|
||||
* @return The highlight theme
|
||||
*/
|
||||
Theme getHighlightTheme();
|
||||
|
||||
/**
|
||||
* Get the theme for a given code.
|
||||
*
|
||||
* @param code
|
||||
* The theme code
|
||||
* @return The theme
|
||||
*/
|
||||
Theme getTheme(final String code);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package gui;
|
||||
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.geom.AffineTransform;
|
||||
|
||||
/**
|
||||
* @param <T> The type of elements in the document
|
||||
*/
|
||||
public interface Cartographer<T>
|
||||
{
|
||||
/**
|
||||
* @param model The document to paint
|
||||
* @param g2 The graphics context to paint on
|
||||
* @param at The affine transform to apply to the graphics context before painting
|
||||
*/
|
||||
public void paintHighlights(final CartographyDocument<T> model, final Graphics2D g2,
|
||||
final AffineTransform at);
|
||||
|
||||
/**
|
||||
* @param model The document to paint
|
||||
* @param g2 The graphics context to paint on
|
||||
* @param at The affine transform to apply to the graphics context before painting
|
||||
*/
|
||||
public void paintShapes(final CartographyDocument<T> model, final Graphics2D g2,
|
||||
final AffineTransform at);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package gui;
|
||||
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @param <T> The type of elements in the document
|
||||
*/
|
||||
public class CartographyDocument<T> implements Iterable<T>
|
||||
{
|
||||
private Map<String, T> highlighted;
|
||||
private Map<String, T> elements;
|
||||
private Rectangle2D.Double bounds;
|
||||
|
||||
/**
|
||||
* @param elements The elements in the document, mapped by their id
|
||||
* @param bounds The bounds of the document
|
||||
*/
|
||||
public CartographyDocument(final Map<String, T> elements, final Rectangle2D.Double bounds)
|
||||
{
|
||||
this.elements = elements;
|
||||
this.bounds = bounds;
|
||||
this.highlighted = Map.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The bounds of the document
|
||||
*/
|
||||
public Rectangle2D.Double getBounds()
|
||||
{
|
||||
return bounds;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id The id of the element to get
|
||||
* @return The element with the given id, or null if no such element exists
|
||||
*/
|
||||
public T getElement(final String id)
|
||||
{
|
||||
return elements.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The iterator of highlighted elements in the document
|
||||
*/
|
||||
public Iterator<T> highlighted()
|
||||
{
|
||||
return highlighted.values().iterator();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The iterator of all elements in the document
|
||||
*/
|
||||
public Iterator<T> iterator()
|
||||
{
|
||||
return elements.values().iterator();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param highlighted The new highlighted elements, mapped by their id
|
||||
*/
|
||||
public void setHighlighted(final Map<String, T> highlighted)
|
||||
{
|
||||
this.highlighted = highlighted;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
package gui;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
import java.awt.geom.*;
|
||||
import java.util.*;
|
||||
|
||||
import math.*;
|
||||
|
||||
/**
|
||||
* A GUI component that can be extended to draw maps of various kinds.
|
||||
*
|
||||
* @param <T> The type of the data
|
||||
*
|
||||
* @author Prof. David Bernstein, James Madison University
|
||||
* @version 1.0
|
||||
*/
|
||||
public class CartographyPanel<T> extends JPanel implements MouseListener, MouseMotionListener
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
protected DisplayCoordinatesTransformation displayTransform;
|
||||
protected LinkedList<Rectangle2D.Double> zoomStack;
|
||||
|
||||
private CartographyDocument<T> model;
|
||||
private Cartographer<T> cartographer;
|
||||
private int[] rbMax, rbMin, rbStart, rbStop; // For the rubber-band-box
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Explicit Value Constructor.
|
||||
*
|
||||
* @param model The model to use
|
||||
* @param cartographer The cartographer to use
|
||||
*/
|
||||
public CartographyPanel(final CartographyDocument<T> model,
|
||||
final Cartographer<T> cartographer)
|
||||
{
|
||||
displayTransform = new DisplayCoordinatesTransformation();
|
||||
|
||||
this.model = model;
|
||||
zoomStack = new LinkedList<Rectangle2D.Double>();
|
||||
zoomStack.add(model.getBounds());
|
||||
|
||||
this.cartographer = cartographer;
|
||||
|
||||
rbStart = new int[2];
|
||||
rbStop = new int[2];
|
||||
rbMax = new int[2];
|
||||
rbMin = new int[2];
|
||||
|
||||
addMouseListener(this);
|
||||
addMouseMotionListener(this);
|
||||
|
||||
setDoubleBuffered(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a de-selection event (i.e., a zoom-out).
|
||||
*/
|
||||
public void handleDeselect()
|
||||
{
|
||||
if (zoomStack.size() > 1)
|
||||
{
|
||||
zoomStack.removeFirst();
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a selection event (i.e., a zoom-in).
|
||||
*
|
||||
* @param min The upper-left of the selection (in screen coordinates)
|
||||
* @param max The lower-right of the selection (in screen coordinates)
|
||||
*/
|
||||
protected void handleSelect(final double[] min, final double[] max)
|
||||
{
|
||||
double scale = displayTransform.getLastTransform().getScaleX();
|
||||
double translateX = displayTransform.getLastTransform().getTranslateX();
|
||||
double translateY = displayTransform.getLastTransform().getTranslateY();
|
||||
|
||||
double x = (min[0] - translateX) / scale;
|
||||
double y = (min[1] - translateY) / scale;
|
||||
double width = (max[0] - translateX) / scale - x;
|
||||
double height = (max[1] - translateY) / scale - y;
|
||||
|
||||
Rectangle2D.Double r = new Rectangle2D.Double(x, y, width, height);
|
||||
zoomStack.addFirst((Rectangle2D.Double)
|
||||
(displayTransform.getLastReflection().createTransformedShape(r).getBounds2D()));
|
||||
repaint();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the zoom stack.
|
||||
*
|
||||
* @param bounds The initial bounds
|
||||
*/
|
||||
private void initializeZoomStack(final Rectangle2D.Double bounds)
|
||||
{
|
||||
zoomStack = new LinkedList<Rectangle2D.Double>();
|
||||
zoomStack.add(bounds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle mouseClicked events.
|
||||
*
|
||||
* @param evt The MouseEvent
|
||||
*/
|
||||
@Override
|
||||
public void mouseClicked(final MouseEvent evt)
|
||||
{
|
||||
if (evt.getClickCount() > 1)
|
||||
{
|
||||
handleDeselect();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle mouseDragged events.
|
||||
*
|
||||
* @param evt The MouseEvent
|
||||
*/
|
||||
@Override
|
||||
public void mouseDragged(final MouseEvent evt)
|
||||
{
|
||||
int over = evt.getX();
|
||||
int down = evt.getY();
|
||||
Graphics g = getGraphics();
|
||||
|
||||
setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
|
||||
|
||||
// Erase the old rubber-band-box
|
||||
g.setXORMode(getBackground());
|
||||
g.setColor(Color.green);
|
||||
g.drawRect(rbMin[0], rbMin[1], rbMax[0]-rbMin[0], rbMax[1]-rbMin[1]);
|
||||
|
||||
// Calculate the coordinates of the new rubber-band-box
|
||||
rbStop[0] = over;
|
||||
rbStop[1] = down;
|
||||
rbMin[0] = Math.min(rbStart[0],rbStop[0]);
|
||||
rbMin[1] = Math.min(rbStart[1],rbStop[1]);
|
||||
rbMax[0] = Math.max(rbStart[0],rbStop[0]);
|
||||
rbMax[1] = Math.max(rbStart[1],rbStop[1]);
|
||||
|
||||
// Draw the new rubber-band-box
|
||||
g.drawRect(rbMin[0], rbMin[1], rbMax[0]-rbMin[0], rbMax[1]-rbMin[1]);
|
||||
g.setPaintMode();
|
||||
g.dispose();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Handle mouseEntered events.
|
||||
*
|
||||
* @param evt The MouseEvent
|
||||
*/
|
||||
@Override
|
||||
public void mouseEntered(final MouseEvent evt)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle mouseExited events.
|
||||
*
|
||||
* @param evt The MouseEvent
|
||||
*/
|
||||
@Override
|
||||
public void mouseExited(final MouseEvent evt)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle mouseMoved events.
|
||||
*
|
||||
* @param evt The MouseEvent
|
||||
*/
|
||||
@Override
|
||||
public void mouseMoved(final MouseEvent evt)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle mousePressed events.
|
||||
*
|
||||
* @param evt The MouseEvent
|
||||
*/
|
||||
@Override
|
||||
public void mousePressed(final MouseEvent evt)
|
||||
{
|
||||
int over = evt.getX();
|
||||
int down = evt.getY();
|
||||
|
||||
rbStart[0] = over;
|
||||
rbStart[1] = down;
|
||||
rbStop[0] = over;
|
||||
rbStop[1] = down;
|
||||
rbMin[0] = over;
|
||||
rbMin[1] = down;
|
||||
rbMax[0] = over;
|
||||
rbMax[1] = down;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle mouseReleased events.
|
||||
*
|
||||
* @param evt The MouseEvent
|
||||
*/
|
||||
@Override
|
||||
public void mouseReleased(final MouseEvent evt)
|
||||
{
|
||||
Graphics g = getGraphics();
|
||||
Dimension d = getSize();
|
||||
g.setClip(0, 0, d.width, d.height);
|
||||
|
||||
|
||||
// Erase the old line
|
||||
g.setXORMode(getBackground());
|
||||
g.setColor(Color.green);
|
||||
g.drawRect(rbMin[0], rbMin[1], rbMax[0]-rbMin[0], rbMax[1]-rbMin[1]);
|
||||
|
||||
// Determine the selected region
|
||||
double[] min = new double[2];
|
||||
min[0] = (double)(rbMin[0]);
|
||||
min[1] = (double)(rbMin[1] - 1);
|
||||
|
||||
double[] max = new double[2];
|
||||
max[0] = (double)(rbMax[0]);
|
||||
max[1] = (double)(rbMax[1] - 1);
|
||||
|
||||
|
||||
// Reset the graphics environment
|
||||
g.setPaintMode();
|
||||
g.dispose();
|
||||
setCursor(Cursor.getDefaultCursor());
|
||||
|
||||
// Notify any children of the selection
|
||||
if ((min[0] != max[0]) || (min[1] != max[1]))
|
||||
{
|
||||
handleSelect(min, max);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Render this component.
|
||||
*
|
||||
* @param g The rendering engine to use
|
||||
*/
|
||||
@Override
|
||||
public void paint(final Graphics g)
|
||||
{
|
||||
Graphics2D g2 = (Graphics2D)g;
|
||||
Rectangle screenBounds = g2.getClipBounds();
|
||||
g2.setColor(getBackground());
|
||||
g2.fill(screenBounds);
|
||||
g2.setColor(Color.BLACK);
|
||||
|
||||
Rectangle2D.Double bounds = zoomStack.getFirst();
|
||||
|
||||
AffineTransform at = displayTransform.getTransform(screenBounds, bounds);
|
||||
|
||||
cartographer.paintShapes(model, g2, at);
|
||||
cartographer.paintHighlights(model, g2, at);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the model.
|
||||
*
|
||||
* @param model The model
|
||||
*/
|
||||
public void setModel(final CartographyDocument<T> model)
|
||||
{
|
||||
this.model = model;
|
||||
initializeZoomStack(model.getBounds());
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
package gui;
|
||||
|
||||
import dataprocessing.*;
|
||||
import feature.Street;
|
||||
import feature.StreetSegmentObserver;
|
||||
import feature.StreetSegmentSubject;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.*;
|
||||
|
||||
/**
|
||||
* A dialog box for a Geocoder.
|
||||
*
|
||||
* @author Prof. David Bernstein, James Madison University
|
||||
* @version 1.0
|
||||
*/
|
||||
public class GeocodeDialog extends JDialog implements
|
||||
ActionListener, ListSelectionListener, StreetSegmentSubject
|
||||
{
|
||||
// Constants with package visibility
|
||||
static final String CATEGORY = "Category";
|
||||
static final String GEOCODE = "Geocode";
|
||||
static final String NAME = "Name";
|
||||
static final String NUMBER = "Number";
|
||||
static final String PREFIX = "Prefix";
|
||||
static final String SUFFIX = "Suffix";
|
||||
|
||||
// Constants with private visibility
|
||||
private static final String QUOTE = "\"";
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
// Attributes
|
||||
private List<String> ids;
|
||||
private List<StreetSegmentObserver> observers;
|
||||
|
||||
private Geocoder geocoder;
|
||||
private JButton geocodeButton;
|
||||
private JComboBox<String> categoryField, prefixField, suffixField;
|
||||
private JList<String> resultsArea;
|
||||
private JTextField nameField, numberField;
|
||||
|
||||
/**
|
||||
* Explicit Value Constructor.
|
||||
*
|
||||
* @param owner The owning JFrame
|
||||
* @param geocoder The Geocoder
|
||||
*/
|
||||
public GeocodeDialog(final JFrame owner, final Geocoder geocoder)
|
||||
{
|
||||
super(owner, "Geocoder", false);
|
||||
this.geocoder = geocoder;
|
||||
|
||||
observers = new ArrayList<StreetSegmentObserver>();
|
||||
|
||||
numberField = createJTextField();
|
||||
prefixField = createJComboBox(null);
|
||||
nameField = createJTextField();
|
||||
categoryField = createJComboBox(null);
|
||||
suffixField = createJComboBox(null);
|
||||
|
||||
geocodeButton = new JButton(GEOCODE);
|
||||
geocodeButton.addActionListener(this);
|
||||
|
||||
resultsArea = new JList<String>(new DefaultListModel<String>());
|
||||
resultsArea.addListSelectionListener(this);
|
||||
|
||||
String[] directions = new String[] {"E","N","NE","NW","S","SE","SW","W"};
|
||||
populate(prefixField, directions);
|
||||
populate(categoryField, new String[] {"Aly","Arc","Ave","Blvd","Br","Brg","Byp","Cir",
|
||||
"Cres","Cswy","Ct","Ctr","Cv","Dr","Expy","Fwy","Grd","Hwy","Ln","Loop","Mal","Pass",
|
||||
"Path","Pike","Pky","Pl","Plz","Ramp","Rd","Row","Rte","Rue","Run","Spur","Sq","St",
|
||||
"Ter","Tpke","Trce","Trl","Tunl","Walk","Way","Xing"});
|
||||
populate(suffixField, directions);
|
||||
|
||||
|
||||
performLayout();
|
||||
setSize(800, 400);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle actionPerformed() messages.
|
||||
*
|
||||
* @param evt The event that generated the message.
|
||||
*/
|
||||
public void actionPerformed(final ActionEvent evt)
|
||||
{
|
||||
DefaultListModel<String> listModel = (DefaultListModel<String>)resultsArea.getModel();
|
||||
listModel.clear();
|
||||
|
||||
String prefix = prefixField.getItemAt(prefixField.getSelectedIndex());
|
||||
String name = nameField.getText();
|
||||
String category = categoryField.getItemAt(categoryField.getSelectedIndex());
|
||||
String suffix = suffixField.getItemAt(suffixField.getSelectedIndex());
|
||||
int number = 0;
|
||||
try
|
||||
{
|
||||
number = Integer.parseInt(numberField.getText());
|
||||
String canonicalName = Street.createCanonicalName(prefix, name, category, suffix);
|
||||
ids = new ArrayList<String>();
|
||||
List<double[]> locations = geocoder.geocode(canonicalName, number, ids);
|
||||
List<String> lonlat = new ArrayList<String>();
|
||||
for (double[] location: locations) lonlat.add(location[0] + "," + location[1]);
|
||||
displayResults(lonlat);
|
||||
}
|
||||
catch (NumberFormatException nfe)
|
||||
{
|
||||
JOptionPane.showMessageDialog(this, QUOTE+numberField.getText()+QUOTE
|
||||
+ " is not a valid street number!",
|
||||
"Input Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a StreetSegmentObserver to this subject.
|
||||
*
|
||||
* @param observer The observer
|
||||
*/
|
||||
@Override
|
||||
public void addStreetSegmentObserver(final StreetSegmentObserver observer)
|
||||
{
|
||||
observers.add(observer);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a JPanel containing a Jcomponent and a titled-border.
|
||||
*
|
||||
* @param title The title
|
||||
* @param component The JComponent
|
||||
* @return The JPanel
|
||||
*/
|
||||
private JPanel createTitledComponent(final String title, final JComponent component)
|
||||
{
|
||||
JPanel result = new JPanel();
|
||||
result.setLayout(new BorderLayout());
|
||||
result.add(component, BorderLayout.CENTER);
|
||||
result.setBorder(BorderFactory.createTitledBorder(title));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a JComboBox.
|
||||
*
|
||||
* @param items The items to include (after the blank)
|
||||
* @return The JComboBox
|
||||
*/
|
||||
private JComboBox<String> createJComboBox(final String[] items)
|
||||
{
|
||||
JComboBox<String> result = new JComboBox<String>();
|
||||
result.setEditable(false);
|
||||
populate(result, items);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a JTextField.
|
||||
*
|
||||
* @return The JTextField
|
||||
*/
|
||||
private JTextField createJTextField()
|
||||
{
|
||||
JTextField result = new JTextField("", 6);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display result.
|
||||
*
|
||||
* @param results The results to display
|
||||
*/
|
||||
private void displayResults(final List<String> results)
|
||||
{
|
||||
DefaultListModel<String> model = (DefaultListModel<String>)resultsArea.getModel();
|
||||
model.clear();
|
||||
if (results != null)
|
||||
{
|
||||
for (String line: results) model.addElement(line);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify all of the StreetSegmentObserver objects of a
|
||||
* handleStreetSegment() message.
|
||||
*
|
||||
* @param segmendIDs The IDs of the StreetSegment objects
|
||||
*/
|
||||
private void notifyStreetSegmentObservers(final List<String> segmendIDs)
|
||||
{
|
||||
for (StreetSegmentObserver observer: observers) observer.handleStreetSegments(segmendIDs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Layout this Component.
|
||||
*
|
||||
*/
|
||||
private void performLayout()
|
||||
{
|
||||
JPanel contentPane = (JPanel)getContentPane();
|
||||
|
||||
contentPane.setLayout(new BorderLayout());
|
||||
|
||||
JToolBar toolBar = new JToolBar();
|
||||
toolBar.setFloatable(false);
|
||||
toolBar.addSeparator();
|
||||
toolBar.add(geocodeButton);
|
||||
contentPane.add(toolBar, BorderLayout.SOUTH);
|
||||
|
||||
Row entryPanel = new Row();
|
||||
entryPanel.add(createTitledComponent(NUMBER, numberField), 1, 0.0);
|
||||
entryPanel.add(createTitledComponent(PREFIX, prefixField), 1, 0.0);
|
||||
entryPanel.add(createTitledComponent(NAME, nameField), 3, 100.0);
|
||||
entryPanel.add(createTitledComponent(CATEGORY, categoryField), 1, 0.0);
|
||||
entryPanel.add(createTitledComponent(SUFFIX, suffixField), 1, 0.0);
|
||||
|
||||
|
||||
JPanel center = new JPanel();
|
||||
center.setLayout(new BorderLayout());
|
||||
center.add(entryPanel, BorderLayout.NORTH);
|
||||
JScrollPane sp = new JScrollPane(resultsArea);
|
||||
sp.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
|
||||
sp.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
|
||||
center.add(sp, BorderLayout.CENTER);
|
||||
|
||||
contentPane.add(center, BorderLayout.CENTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate a JComboBox.
|
||||
*
|
||||
* @param comboBox The JComboBox to populate
|
||||
* @param items The items to populate it with (or null to reset)
|
||||
*/
|
||||
private void populate(final JComboBox<String> comboBox, final String[] items)
|
||||
{
|
||||
comboBox.removeAllItems();
|
||||
comboBox.addItem("");
|
||||
if (items != null)
|
||||
{
|
||||
Arrays.sort(items);
|
||||
for (String item: items) comboBox.addItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a StreetSegmentObserver from this subject.
|
||||
*
|
||||
* @param observer The observer
|
||||
*/
|
||||
@Override
|
||||
public void removeStreetSegmentObserver(final StreetSegmentObserver observer)
|
||||
{
|
||||
observers.remove(observer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the selection mode (e.g., ListSelectionModel.SINGLE_SELECTION) for the JList.
|
||||
*
|
||||
* @param mode The selection mode
|
||||
*/
|
||||
public void setSelectionMode(final int mode)
|
||||
{
|
||||
resultsArea.setSelectionMode(mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle valueChanged() messages.
|
||||
*
|
||||
* @param evt The event that generated the message
|
||||
*/
|
||||
@Override
|
||||
public void valueChanged(final ListSelectionEvent evt)
|
||||
{
|
||||
if (!evt.getValueIsAdjusting())
|
||||
{
|
||||
int indexes[] = resultsArea.getSelectedIndices();
|
||||
List<String> selectedIDs = new ArrayList<String>();
|
||||
for (int i=0; i<indexes.length; i++)
|
||||
{
|
||||
selectedIDs.add(ids.get(indexes[i]));
|
||||
}
|
||||
notifyStreetSegmentObservers(selectedIDs);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package gui;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.geom.AffineTransform;
|
||||
|
||||
import geography.GeographicShape;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class GeographicShapeCartographer implements Cartographer<GeographicShape>
|
||||
{
|
||||
private Color color;
|
||||
|
||||
/**
|
||||
* @param color
|
||||
* The color to use when rendering the shapes.
|
||||
*/
|
||||
public GeographicShapeCartographer(final Color color)
|
||||
{
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void paintHighlights(final CartographyDocument<GeographicShape> model, final Graphics2D g2,
|
||||
final AffineTransform at)
|
||||
{
|
||||
g2.setColor(new Color(color.getRed(), color.getGreen(), color.getBlue(), 128));
|
||||
GeographicShape shape;
|
||||
while (model.highlighted().hasNext())
|
||||
{
|
||||
shape = model.highlighted().next();
|
||||
g2.fill(at.createTransformedShape(shape.getShape()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void paintShapes(final CartographyDocument<GeographicShape> model, final Graphics2D g2,
|
||||
final AffineTransform at)
|
||||
{
|
||||
g2.setColor(color);
|
||||
for (GeographicShape shape : model)
|
||||
{
|
||||
g2.draw(at.createTransformedShape(shape.getShape()));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package gui;
|
||||
|
||||
import java.awt.*;
|
||||
import javax.swing.*;
|
||||
|
||||
/**
|
||||
* A JPanel that is layed-out in the horizontal dimension.
|
||||
* Individual elements can be sized relative to each other and
|
||||
* their "expandability" can be specified.
|
||||
*
|
||||
* @author Prof. David Bernstein, James Madsion University
|
||||
* @version 1.0
|
||||
*/
|
||||
public class Row extends JPanel
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private GridBagLayout layout;
|
||||
private int column;
|
||||
|
||||
|
||||
/**
|
||||
* Default Constructor.
|
||||
*/
|
||||
public Row()
|
||||
{
|
||||
super();
|
||||
column = 0;
|
||||
layout = new GridBagLayout();
|
||||
setLayout(layout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a Component to this Row.
|
||||
* Note: The expandability of all Component objects normally sum to 100.
|
||||
*
|
||||
* @param component The Component to add
|
||||
* @param width The width (in cells) of this Component
|
||||
* @param expandability The percentage of extra space that this Component should use
|
||||
*/
|
||||
public void add(final Component component, final int width, final double expandability)
|
||||
{
|
||||
GridBagConstraints gbc = new GridBagConstraints();
|
||||
gbc.gridx = column;
|
||||
gbc.gridy = 0;
|
||||
gbc.gridwidth = width;
|
||||
gbc.gridheight = 1;
|
||||
if (expandability > 0.0) gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
else gbc.fill = GridBagConstraints.NONE;
|
||||
gbc.weightx = expandability;
|
||||
layout.setConstraints(component, gbc);
|
||||
add(component);
|
||||
|
||||
column += width;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package gui;
|
||||
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.util.Iterator;
|
||||
|
||||
import feature.StreetSegment;
|
||||
import feature.StreetThemeLibrary;
|
||||
import geography.Theme;
|
||||
import geography.ThemeLibrary;
|
||||
|
||||
/**
|
||||
* A cartographer for rendering street segments.
|
||||
*/
|
||||
public class StreetSegmentCartographer implements Cartographer<StreetSegment>
|
||||
{
|
||||
private ThemeLibrary themeLibrary = new StreetThemeLibrary();
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public StreetSegmentCartographer()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void paintHighlights(final CartographyDocument<StreetSegment> model, final Graphics2D g2,
|
||||
final AffineTransform at)
|
||||
{
|
||||
StreetSegment segment;
|
||||
Iterator<StreetSegment> it = model.highlighted();
|
||||
while (it.hasNext())
|
||||
{
|
||||
segment = it.next();
|
||||
Theme theme = themeLibrary.getHighlightTheme();
|
||||
g2.setColor(theme.getColor());
|
||||
g2.setStroke(theme.getStroke());
|
||||
g2.draw(at.createTransformedShape(segment.getGeographicShape().getShape()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void paintShapes(final CartographyDocument<StreetSegment> model, final Graphics2D g2,
|
||||
final AffineTransform at)
|
||||
{
|
||||
for (StreetSegment segment : model)
|
||||
{
|
||||
Theme theme = themeLibrary.getTheme(segment.getCode());
|
||||
g2.setColor(theme.getColor());
|
||||
g2.setStroke(theme.getStroke());
|
||||
g2.draw(at.createTransformedShape(segment.getGeographicShape().getShape()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package math;
|
||||
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
|
||||
/**
|
||||
* A view transformation that maps content coordinates to display coordinates, while maintaining the
|
||||
* aspect ratio of the content. The content is reflected across the horizontal axis to match the
|
||||
* typical display coordinate system where the y-axis increases downwards. The transformation also
|
||||
* centers the content within the display bounds.
|
||||
*/
|
||||
public class DisplayCoordinatesTransformation implements ViewTransformation
|
||||
{
|
||||
private AffineTransform lastReflection = new AffineTransform();
|
||||
private AffineTransform lastTransform = new AffineTransform();
|
||||
|
||||
@Override
|
||||
public AffineTransform getLastReflection()
|
||||
{
|
||||
return new AffineTransform(lastReflection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AffineTransform getLastTransform()
|
||||
{
|
||||
return new AffineTransform(lastTransform);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AffineTransform getTransform(final Rectangle2D displayBounds,
|
||||
final Rectangle2D contentBounds)
|
||||
{
|
||||
double scaleX = displayBounds.getWidth() / contentBounds.getWidth();
|
||||
double scaleY = displayBounds.getHeight() / contentBounds.getHeight();
|
||||
double scale = Math.min(scaleX, scaleY);
|
||||
|
||||
double scaledHeight = contentBounds.getHeight() * scale;
|
||||
|
||||
lastReflection = new AffineTransform();
|
||||
lastReflection.translate(0.0, contentBounds.getMinY() + contentBounds.getMaxY());
|
||||
lastReflection.scale(1.0, -1.0);
|
||||
|
||||
double scaledWidth = contentBounds.getWidth() * scale;
|
||||
|
||||
double offsetX = displayBounds.getMinX() + (displayBounds.getWidth() - scaledWidth) / 2.0;
|
||||
double offsetY = displayBounds.getMinY() + (displayBounds.getHeight() - scaledHeight) / 2.0;
|
||||
|
||||
double translateX = offsetX - contentBounds.getMinX() * scale;
|
||||
double translateY = offsetY - contentBounds.getMinY() * scale;
|
||||
|
||||
lastTransform = AffineTransform.getTranslateInstance(translateX, translateY);
|
||||
lastTransform.scale(scale, scale);
|
||||
|
||||
AffineTransform combined = new AffineTransform(lastTransform);
|
||||
combined.concatenate(lastReflection);
|
||||
|
||||
return combined;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package math;
|
||||
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public interface ViewTransformation
|
||||
{
|
||||
/**
|
||||
* @return The last reflection.
|
||||
*/
|
||||
public AffineTransform getLastReflection();
|
||||
|
||||
/**
|
||||
* @return The last transform.
|
||||
*/
|
||||
public AffineTransform getLastTransform();
|
||||
|
||||
/**
|
||||
* @param displayBounds
|
||||
* The bounds of the display area
|
||||
* @param contentBounds
|
||||
* The bounds of the content area
|
||||
* @return The transform that maps content coordinates to display coordinates, while maintaining
|
||||
* the aspect ratio of the content. The content is reflected across the horizontal axis to
|
||||
* match the typical display coordinate system where the y-axis increases downwards. The
|
||||
* transformation also centers the content within the display bounds.
|
||||
*/
|
||||
public AffineTransform getTransform(final Rectangle2D displayBounds,
|
||||
final Rectangle2D contentBounds);
|
||||
}
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../assets/usa48-counties.geo
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../assets/usa48-states.geo
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../assets/va-blocks.geo
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../assets/va-counties.geo
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../assets/va-streets-2024.geo
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../assets/va-streets-2024.str
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../assets/world-countries.geo
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<fileset-config file-format-version="1.2.0" simple-config="true" sync-formatter="false">
|
||||
<fileset name="all" enabled="true" check-config-name="CS480StyleGuide" local="false">
|
||||
<file-match-pattern match-pattern="." include-pattern="true"/>
|
||||
</fileset>
|
||||
</fileset-config>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
|
||||
<classpathentry kind="src" path="src"/>
|
||||
<classpathentry kind="output" path="bin"/>
|
||||
</classpath>
|
||||
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>PA4</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>net.sf.eclipsecs.core.CheckstyleBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
<nature>net.sf.eclipsecs.core.CheckstyleNature</nature>
|
||||
</natures>
|
||||
<filteredResources>
|
||||
<filter>
|
||||
<id>1775716684331</id>
|
||||
<name></name>
|
||||
<type>30</type>
|
||||
<matcher>
|
||||
<id>org.eclipse.core.resources.regexFilterMatcher</id>
|
||||
<arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
|
||||
</matcher>
|
||||
</filter>
|
||||
</filteredResources>
|
||||
</projectDescription>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user