diff --git a/lectures/build.gradle b/lectures/build.gradle index 13f17387144c256fca1498f744148dc6f2c04e96..37dec022b85f27e954575cb1f89a6c597f18a1c1 100644 --- a/lectures/build.gradle +++ b/lectures/build.gradle @@ -61,7 +61,7 @@ asciidoctorRevealJs { outputDir file('build/docs/revealjs') resources { from('revealjs') { - include '**/*.png' + include 'images/*' include '**/*.css' } into '.' diff --git a/lectures/revealjs/agiletools.adoc b/lectures/revealjs/agiletools.adoc index aff88dae690f8df4003c449f2335ffab83942de9..4ef39ecca5bde7ff954d806cb819f31e2c6903d0 100644 --- a/lectures/revealjs/agiletools.adoc +++ b/lectures/revealjs/agiletools.adoc @@ -63,7 +63,7 @@ Hva har dette å si for verktøystøtte? == `gitlab.stud.idi...` -[.smaller] +[.smaller-80] * profesjonelt støtteverktøy for smidig utvikling * IDI har egen installasjon (tas kanskje over av NTNU IT) ** innlogging med NTNU-navn og -passord @@ -93,7 +93,7 @@ Oppgavesporing er viktig for transparens == Oppgavesporing forts. -[.smaller] +[.smaller-80] * oppgaver opprettes ifm. planlegging av iterasjon, f.eks. fra _brukerhistorier_, _funksjonsønsker_ eller _feilrapporter_ * oppgaver knyttes til ** _milepæl_ for iterasjon diff --git a/lectures/revealjs/ci.adoc b/lectures/revealjs/ci.adoc index e1389f36589737d8927bcde10dbd786f215531b4..1a87d4ecf081dff26ee1ce9abbbff9381a9312fd 100644 --- a/lectures/revealjs/ci.adoc +++ b/lectures/revealjs/ci.adoc @@ -1,3 +1,153 @@ -= CI += Kontinuerlig integrasjon :customcss: slides.css :icons: font + +== Kontinuerlig integrasjon (CI) + +Automatisering av alt som fremmer kvalitet, men som tar tid, f.eks. + +* kompilering og bygging av jar-filer (generelt avledete artifakter) +* kjøring av enhets- og integrasjonstester +* analyser av ulike typer kvalitet (formatering, kodingsstandarder, vanlige feil, ...) +* bygging av kjørbart system og kjøring av systemtester + +== Smidig utfordring + +[.smaller-60] +* Hvordan iterere raskt? +** skrive korrekt kode raskt +** være trygg på kvaliteten +** levere ofte, for å få tilbakemelding fra brukere + +[.smaller-60] +* Mange nivåer av testing +** egen kode - enhetstesting +** koden innen teamet - integrasjonstesting +** hele systemet - systemtesting (og evt. deployment) + +== Smidig løsning + +[.smaller-80] +* Kontinuerlig - bygg, sett sammen og test +* Innimellom - lever (release) og sett i drift/prod. (deploy) +* Alt for mye arbeid uten støtte +** _byggeverktøy_ automatiserer prosessen +** _byggetjenere_ sikrer reproduserbar prosess + +== Byggeverktøy + +[.smaller-80] +* strukturert tilnærming basert på meta-informasjon +** prosjekt-type (språk, plattform, ...) +** struktur og avhengigeter +** rammeverk og bibliotek automatisk lastet fra nettet +* sekvens av byggetrinn med innbyrdes avhengigheter +* mange konfigurasjonsmuligheter med gode standardinnstillinger (convention over configuration) +* prosjektmaler gjør initiell rigging enklere +* i stor grad styrt av tillegg (plugins) til et generell system + +== Byggeverktøy forts. + +[.smaller-60] +* to hovedalternativer: *gradle* og *maven* +** litt ulik terminologi og logikk, men forskjellene er tildels overfladiske +** begge kan mye det samme, så valget er i stor grad pragmatisk (!) +** masse materiale å lære fra, uansett valg +* *maven* +** mer modent og strukturert, men også mer krevende og tungvint +* *gradle* +** konfigurerbare byggetrinn sekvensieres basert på avhengigheter +** konfig.filene (*settings.gradle* og *build.gradle*) er forståelige + +== Enkelt prosjektoppsett + +[.smaller-60] +Enkle prosjekter består av én modul og mappe + +[.smaller-60] +* *settings.gradle* og *build.gradle* konfigurerer bygg +* *gradlew*/*gradlew.bat* og *gradle*-mappe må inkluderes + +[.smaller-60] +Eksempel: + +[.smaller-60] +* desktop-app med javafx og fxml +* ikke noe behov for gjenbruk av kjernelogikk i andre prosjekter + +== simpleexample + +[.smaller] +[.left-60] +* eksempel på én-modul-prosjekt +** javafx+fxml-app +** standard mapper for java +** gradle-filer + +[.right] +image::../images/simpleexample-structure.png[width=300] + +== simpleexample forts. + +[.smaller] +* standard innhold i *build.gradle* +** plugins - tillegg som passer til typen prosjekt, f.eks. +** repositories - hvor finnes tillegg og bibliotek (jar-filer) +** dependencies - hvilke bibliotek brukes og til hva/når +* tillegg-spesifikk konfigurasjon, f.eks. +** oppstartsklasse for java +** javafx-versjon og -moduler som brukes + +== simpleexample forts. + +* build.gradle *plugins* +** application - java-app med main-metode, som kan pakkes som kjørbar jar +** org.openjfx.javafxplugin - javafx-app + +[source] +---- +plugins { + id 'application' + id 'org.openjfx.javafxplugin' version '0.0.8' +} +---- + +== Modulært prosjektoppsett + +[.smaller-60] +_Flermodul_-prosjekter har delmoduler i undermapper + +[.smaller-60] +* *settings.gradle* i hovedmappa navngir hovedprosjektet og delmodulene +* *gradlew*/*gradlew.bat* og *gradle*-mappe må inkluderes i hovedmappa +* delmodulene (i undermappene) trenger (bare) egen *build.gradle*-fil + +[.smaller-60] +Eksempel: + +[.smaller-60] +* klient-tjener-app +* (noe) felles kjernelogikk i klient og tjener +* kan ha flere klienter, f.eks. desktop og mobil (og web) + +== Modularitet + +[.smaller-60] +* et modulært system er mer oversiktlig +** moduler er relativt selvstendige enheter +** eksplisitte avhengigheter mellom moduler +** gjenbrukes på tvers av prosjekter +* tilsvarer i java et sett pakker +** skiller mellom API og interne pakker +** distribueres som jar-fil (zip-fil med meta-data) + +== multiexample + +[.smaller] +[.left-60] +* eksempel på fler-modul-prosjekt +** core - kjernelogikk +** fx - javafx+fxml-app +** jaxrs - REST API +** jersey - web-server-app +** webreact - web-klient diff --git a/lectures/revealjs/images/git-branching.png b/lectures/revealjs/images/git-branching.png new file mode 100644 index 0000000000000000000000000000000000000000..4f3c85fabeaee04837f6f136209103aa70e149ef Binary files /dev/null and b/lectures/revealjs/images/git-branching.png differ diff --git a/lectures/revealjs/images/git-local-remote-repo.png b/lectures/revealjs/images/git-local-remote-repo.png new file mode 100644 index 0000000000000000000000000000000000000000..adcc8387c22d7cc0439dc1d08603a4dcdb8bfa9b Binary files /dev/null and b/lectures/revealjs/images/git-local-remote-repo.png differ diff --git a/lectures/revealjs/images/git-repo-commands.png b/lectures/revealjs/images/git-repo-commands.png new file mode 100644 index 0000000000000000000000000000000000000000..3d8c1f6c6e59d3548f76e41002a9f3287cb19e9a Binary files /dev/null and b/lectures/revealjs/images/git-repo-commands.png differ diff --git a/lectures/revealjs/images/git-repo-copies.png b/lectures/revealjs/images/git-repo-copies.png new file mode 100644 index 0000000000000000000000000000000000000000..112478fff1be958aebc031536c8ca6542234d0d1 Binary files /dev/null and b/lectures/revealjs/images/git-repo-copies.png differ diff --git a/lectures/revealjs/images/git-repositories-view.png b/lectures/revealjs/images/git-repositories-view.png new file mode 100644 index 0000000000000000000000000000000000000000..d05160d7294f7c47cefaa6196a104ed9fa5fd8ca Binary files /dev/null and b/lectures/revealjs/images/git-repositories-view.png differ diff --git a/lectures/revealjs/images/git-staging-view.png b/lectures/revealjs/images/git-staging-view.png new file mode 100644 index 0000000000000000000000000000000000000000..d6c307efd5c9f92f9e2652d66c1b0fb8668704d6 Binary files /dev/null and b/lectures/revealjs/images/git-staging-view.png differ diff --git a/lectures/revealjs/images/simpleexample-structure.png b/lectures/revealjs/images/simpleexample-structure.png new file mode 100644 index 0000000000000000000000000000000000000000..fdf2532cb6abcd865a7dbfc14d2adbeca52e6e4b Binary files /dev/null and b/lectures/revealjs/images/simpleexample-structure.png differ diff --git a/lectures/revealjs/scm.adoc b/lectures/revealjs/scm.adoc index 33bde303b58bd583db9dd3572139a90e73d73322..d3bea3e52f1e599776d5267a7baa6deebd85f539 100644 --- a/lectures/revealjs/scm.adoc +++ b/lectures/revealjs/scm.adoc @@ -1,3 +1,124 @@ -= SCM += SCM - Source Code Management :customcss: slides.css :icons: font + +== Kildekodehåndtering + +* lagring av kode +* sporing av endringer +* versionering +* distribusjon + +[.grid-left-right-50-50] +== `git` + +[.smaller-60] +[.area-left] +* sporing og deling av kodeendringer i såkalte _repo_ (repository) +* både sentrale og lokale repo har all endringshistorikk +* initiell kopi hentes ved å _kloning_ (clone) +* endringer (i filer) registreres i _commits_ +* commits deles med andre ved å _dytte_ (push) til en server +* andre kan da _dra_ (pull) endringene inn i lokalt repo + +[.area-right] +image::../images/git-local-remote-repo.png[] + +[.grid-left-right-50-50] +== 4 repo-"kopier" + +[.smaller-60] +[.area-left] +* originalen (_origin_), hentet fra server (`pull`) +* arbeidsområdet (_working directory_), lokale filer som kan være endret +* indeksen (_staging area_ eller _index_), endringer du har lagt til med (`add`) +* lokalt repo, endringene du har commitet (`commit`) + +[.area-right] +image::../images/git-repo-copies.png[] + +== repo-kommandoer + +image::../images/git-repo-copies.png[width="400px"] +image::../images/git-repo-commands.png[width="500px"] + +== Typisk sekvens + +[.smaller-80] +* `git pull` (eller `clone` første gang) - henter ned endringer fra serveren +* gjør egne endringer +* `git status` - viser hva som er endret +* `git add <fil eller mappe>` - legger endringer til fremtidig commit +* `git commit -m <melding, m/oppgavenummer>` - registrerer all endringene (lagt til med `add`) +* `git pull` - henter andres endringer, i tilfelle konflikt +* `git push` - deler endringer med andre via serveren + +== Forgreining (branching) + +[.smaller-60] +[.left-70] +* greiner (branches) +** sporer egne utviklingstråder +** gjør jobbing i parallell ryddigere +* sammenslåing (merging) +** en (hoved)grein slås gjerne sammen med en annen, når den andre er ferdig (nok) +** etter sammenslåing, så deles gjerne resultatet med andre +** andre henter så ned for å være oppdatert + +[.right] +image::../images/git-branching.png[width="200px"] + +== Sekvens m/forgreining + +[.smaller-80] +* `git pull` - henter ned endringer fra serveren +* `git checkout -b <navn på (ny) grein>` - lager ny grein, med utgangspunkt i den du jobber med +* gjør egne endringer +* `git add ...` og `git commit ...` - registrerer all endringene +* `git checkout master` - bytter til hovedgrein +* `git merge <navn på grein>` - slår annen grein (den nye) sammen med denne (hovedgreina) +* ... + +== Eclipse-støtte for git + +[.smaller-60] +* Egne Eclipse-tillegg - JGit (git-impl) og EGit (UI) +* Git Repositories-panel - oversikt over repo +** klone repo fra server eller registrere lokalt repo +** utføre `pull` og `push` +** utføre `checkout` inkl. lage ny grein +** ... + +image::../images/git-repositories-view.png[height="200px"] + +== Eclipse-støtte forts. + +[.smaller-60] +* Git Staging - statusoversikt +** filer i arbeidsområdet som er endret +** filene i indeksen +** legge til (`add`) eller fjerne (`remove`) fra indeksen + +image::../images/git-staging-view.png[width="800px"] + +== Endringsforespørsler + +[.smaller-60] +* en `pull`/`merge`-forespørsel (request) brukes for mer formell godkjenning av endringer +** Pull Request er github-termen, mens Merge Request brukes i gitlab +* brukes ofte for å inkludere endringer utenfra, f.eks. +** utviklere utenfor kjerne-teamet +** brukere av åpen kildekode som har fikset feil +* forenkler administrasjon av åpne prosjekter + +== PR/MR-prosedyre + +[.smaller-60] +* (utenforstående oppretter kopi på egen server) +* lager en grein for endringene og utfører dem lokalt +* i stedet for å slå sammen med egen hovedgrein +** `push` grein til egen server +** lag en `pull`/`merge`-forespørsel (PR/MR), som (potensielt) kan inkluderes i hovedgreina +** UI for dette finnes på github/i gitlab +** forespørselen får en egen dialog/diskusjonstråd +** en utvikler med rettigheter kan så godkjenne evt. avslå forespørsel diff --git a/lectures/revealjs/slides.css b/lectures/revealjs/slides.css index 6733f42d8b9c7149f3dd54d86103d08fd301fa25..2b45fec32a3b24c24757f0e26a441c2daa0d126b 100644 --- a/lectures/revealjs/slides.css +++ b/lectures/revealjs/slides.css @@ -10,6 +10,166 @@ text-align: center; } -.smaller { - font-size: 80% +.smaller-80 { + font-size: 80%; +} + +.smaller-60 { + font-size: 60%; +} + +.left { + float: left; +} + +.left-90 { + float: left; + width: 90%; +} + +.left-80 { + float: left; + width: 80%; +} + +.left-70 { + float: left; + width: 70%; +} + +.left-60 { + float: left; + width: 60%; +} + +.left-50 { + float: left; + width: 50%; +} + +.left-40 { + float: left; + width: 40%; +} + +.left-30 { + float: left; + width: 30%; +} + +.right { + float: right; +} + +.right-80 { + float: right; + width: 80%; +} + +.right-70 { + float: right; + width: 70%; +} + +.right-60 { + float: right; + width: 60%; +} + +.right-50 { + float: right; + width: 50%; +} + +.right-40 { + float: right; + width: 40%; +} + +.right-30 { + float: right; + width: 30%; +} + +section h2 { + grid-area: slide-title; +} + +.reveal h2 { + text-transform: none; +} + +.width-10 { + width: 10%; +} +.width-20 { + width: 20%; +} +.width-30 { + width: 30%; +} +.width-40 { + width: 40%; +} +.width-50 { + width: 50%; +} +.width-60 { + width: 60%; +} +.width-70 { + width: 70%; +} +.width-80 { + width: 80%; +} +.width-90 { + width: 90%; +} + +.grid-left-right { + display: grid !important; + grid-template-areas: 'slide-title slide-title' 'left right'; +} +.grid-left-right-50-50 { + display: grid !important; + grid-template-areas: 'slide-title slide-title' 'left right'; + grid-template-columns: 50% 50% +} + +.area-left { + grid-area: left; +} +.area-right { + grid-area: right; +} + +.grid-compass { + display: grid !important; + grid-template-areas: 'slide-title slide-title slide-title' 'north-west north north-east' 'west middle east' 'south-west south south-east'; +} + +.area-north-west { + grid-area: north-west; +} +.area-north { + grid-area: north; +} +.area-north-east { + grid-area: north-east; +} +.area-west { + grid-area: west; +} +.area-east { + grid-area: east; +} +.area-south-west { + grid-area: south-west; +} +.area-south { + grid-area: south; +} +.area-south-east { + grid-area: south-east; } diff --git a/simpleexample/.classpath b/simpleexample/.classpath new file mode 100644 index 0000000000000000000000000000000000000000..10d994043387f57590a3a567df89e601500658ba --- /dev/null +++ b/simpleexample/.classpath @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry kind="src" output="bin/main" path="src/main/java"> + <attributes> + <attribute name="gradle_scope" value="main"/> + <attribute name="gradle_used_by_scope" value="main,test"/> + </attributes> + </classpathentry> + <classpathentry kind="src" output="bin/main" path="src/main/resources"> + <attributes> + <attribute name="gradle_scope" value="main"/> + <attribute name="gradle_used_by_scope" value="main,test"/> + </attributes> + </classpathentry> + <classpathentry kind="src" output="bin/test" path="src/test/java"> + <attributes> + <attribute name="gradle_scope" value="test"/> + <attribute name="gradle_used_by_scope" value="test"/> + </attributes> + </classpathentry> + <classpathentry kind="src" output="bin/test" path="src/test/resources"> + <attributes> + <attribute name="gradle_scope" value="test"/> + <attribute name="gradle_used_by_scope" value="test"/> + </attributes> + </classpathentry> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-12/"/> + <classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/> + <classpathentry kind="output" path="bin/default"/> +</classpath> diff --git a/simpleexample/.gitignore b/simpleexample/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..837b6f64147e83239994f1736b624de993f36476 --- /dev/null +++ b/simpleexample/.gitignore @@ -0,0 +1,6 @@ +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build/ +bin/ diff --git a/simpleexample/.gitignore~ b/simpleexample/.gitignore~ new file mode 100644 index 0000000000000000000000000000000000000000..1b6985c0094c8e3db5f1c6e2c4d66b82f325284f --- /dev/null +++ b/simpleexample/.gitignore~ @@ -0,0 +1,5 @@ +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build diff --git a/simpleexample/.project b/simpleexample/.project new file mode 100644 index 0000000000000000000000000000000000000000..95eb911346f50860ea21d5eee970164e9e41e8c0 --- /dev/null +++ b/simpleexample/.project @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>simpleexample</name> + <comment>Project simpleexample created by Buildship.</comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.jdt.core.javabuilder</name> + <arguments> + </arguments> + </buildCommand> + <buildCommand> + <name>org.eclipse.buildship.core.gradleprojectbuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.jdt.core.javanature</nature> + <nature>org.eclipse.buildship.core.gradleprojectnature</nature> + </natures> +</projectDescription> diff --git a/simpleexample/.settings/org.eclipse.buildship.core.prefs b/simpleexample/.settings/org.eclipse.buildship.core.prefs new file mode 100644 index 0000000000000000000000000000000000000000..e8895216fd3c0c3af4c4522334775f41b7deb42e --- /dev/null +++ b/simpleexample/.settings/org.eclipse.buildship.core.prefs @@ -0,0 +1,2 @@ +connection.project.dir= +eclipse.preferences.version=1 diff --git a/simpleexample/build.gradle b/simpleexample/build.gradle new file mode 100644 index 0000000000000000000000000000000000000000..01d55f6e311f372a9e58d5b5211567833c38c745 --- /dev/null +++ b/simpleexample/build.gradle @@ -0,0 +1,33 @@ +plugins { + id 'application' + id 'org.openjfx.javafxplugin' version '0.0.8' +} + +repositories { + jcenter() + maven { + url "https://plugins.gradle.org/m2/" + } + mavenLocal() +} + +mainClassName = 'simpleex.ui.FxApp' // application plugin + +// javafx specific way of specifying dependencies +javafx { + version = '12' + modules = [ 'javafx.controls', 'javafx.fxml' ] + // run with --debug-jvm flag and + // launch debugger using Remove Java Application debug launch configuration +} + + +dependencies { + compile "com.fasterxml.jackson.core:jackson-databind:2.9.8" + compile 'fischer.clemens:FxMapControl:1.0' + + testImplementation 'junit:junit:4.12' + testImplementation "org.testfx:testfx-core:4.0.16-alpha" + testImplementation "org.testfx:testfx-junit:4.0.16-alpha" + testImplementation "org.mockito:mockito-core:2.28.2" +} diff --git a/simpleexample/gradle/wrapper/gradle-wrapper.jar b/simpleexample/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..5c2d1cf016b3885f6930543d57b744ea8c220a1a Binary files /dev/null and b/simpleexample/gradle/wrapper/gradle-wrapper.jar differ diff --git a/simpleexample/gradle/wrapper/gradle-wrapper.properties b/simpleexample/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000000000000000000000000000000000..5f1b1201a7353e7a48f40c490e7c799a6318d02e --- /dev/null +++ b/simpleexample/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.4-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/simpleexample/gradlew b/simpleexample/gradlew new file mode 100755 index 0000000000000000000000000000000000000000..b0d6d0ab5deb588123dd658f0b079934ee05a72e --- /dev/null +++ b/simpleexample/gradlew @@ -0,0 +1,188 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/simpleexample/gradlew.bat b/simpleexample/gradlew.bat new file mode 100644 index 0000000000000000000000000000000000000000..15e1ee37a70d7dfdfd8cf727022e117b6f6153a7 --- /dev/null +++ b/simpleexample/gradlew.bat @@ -0,0 +1,100 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem http://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/simpleexample/settings.gradle b/simpleexample/settings.gradle new file mode 100644 index 0000000000000000000000000000000000000000..c6dd73e548547a2e83b46363d8470c7224cd669a --- /dev/null +++ b/simpleexample/settings.gradle @@ -0,0 +1,10 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * + * Detailed information about configuring a multi-project build in Gradle can be found + * in the user manual at https://docs.gradle.org/5.4/userguide/multi_project_builds.html + */ + +rootProject.name = 'simpleexample' diff --git a/simpleexample/src/main/java/fxutil/doc/AbstractDocumentStorageImpl.java b/simpleexample/src/main/java/fxutil/doc/AbstractDocumentStorageImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..13bc9e940ad4744dd9b5aa3b07e6e3ddf8b956f2 --- /dev/null +++ b/simpleexample/src/main/java/fxutil/doc/AbstractDocumentStorageImpl.java @@ -0,0 +1,145 @@ +package fxutil.doc; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; + +/** + * Incomplete implementation of **IDocumentStorage**, to simplify implementing ones for specific document and location types. + * The main missing methods are for getting and setting the current document, creating an empty one and + * creating an **InputStream** from a location. + * @author hal + * + * @param <D> the document type + * @param <L> the location type + */ +public abstract class AbstractDocumentStorageImpl<D, L> implements IDocumentStorage<L>, IDocumentPersistence<D, L> { + + private L documentLocation; + + @Override + public L getDocumentLocation() { + return documentLocation; + } + + @Override + public void setDocumentLocation(final L documentLocation) { + final L oldDocumentLocation = this.documentLocation; + this.documentLocation = documentLocation; + fireDocumentLocationChanged(oldDocumentLocation); + } + + protected void setDocumentAndLocation(final D document, final L documentLocation) { + setDocument(document); + setDocumentLocation(documentLocation); + } + + /** + * Returns the current document. + * @return the current document + */ + protected abstract D getDocument(); + + /** + * Sets the current document + * @param document the new document + */ + protected abstract void setDocument(D document); + + // + + private final Collection<IDocumentStorageListener<L>> documentListeners = new ArrayList<IDocumentStorageListener<L>>(); + + @Override + public void addDocumentStorageListener(final IDocumentStorageListener<L> documentStorageListener) { + documentListeners.add(documentStorageListener); + } + + @Override + public void removeDocumentStorageListener(final IDocumentStorageListener<L> documentStorageListener) { + documentListeners.remove(documentStorageListener); + } + + protected void fireDocumentLocationChanged(final L oldDocumentLocation) { + for (final IDocumentStorageListener<L> documentStorageListener : documentListeners) { + documentStorageListener.documentLocationChanged(documentLocation, oldDocumentLocation); + } + } + + protected void fireDocumentChanged(final D oldDocument) { + for (final IDocumentStorageListener<L> documentListener : documentListeners) { + if (documentListener instanceof IDocumentListener) { + ((IDocumentListener<D, L>) documentListener).documentChanged(getDocument(), oldDocument); + } + } + } + + /** + * Creates a new and empty document. + * @return + */ + protected abstract D createDocument(); + + @Override + public void newDocument() { + setDocumentAndLocation(createDocument(), null); + } + + /** + * Creates an ImportStream from a location + * @param location + * @return + * @throws IOException + */ + protected abstract InputStream toInputStream(L location) throws IOException; + + protected InputStream toInputStream(final File location) throws IOException { + return new FileInputStream(location); + } + + protected InputStream toInputStream(final URL location) throws IOException { + return location.openStream(); + } + + protected InputStream toInputStream(final URI location) throws IOException { + return toInputStream(location.toURL()); + } + + @Override + public void openDocument(final L storage) throws IOException { + try (InputStream input = toInputStream(storage)){ + setDocumentAndLocation(loadDocument(input), storage); + } catch (final Exception e) { + throw new IOException(e); + } + } + + @Override + public void saveDocument() throws IOException { + try { + saveDocument(getDocument(), getDocumentLocation()); + } catch (final Exception e) { + throw new IOException(e); + } + } + + public void saveDocumentAs(final L documentLocation) throws IOException { + final L oldDocumentLocation = getDocumentLocation(); + setDocumentLocation(documentLocation); + try { + saveDocument(); + } catch (final IOException e) { + setDocumentLocation(oldDocumentLocation); + throw e; + } + } + + public void saveCopyAs(final L documentLocation) throws Exception { + saveDocument(getDocument(), documentLocation); + } +} diff --git a/simpleexample/src/main/java/fxutil/doc/FileMenuController.java b/simpleexample/src/main/java/fxutil/doc/FileMenuController.java new file mode 100644 index 0000000000000000000000000000000000000000..3aef54e7f7fb4b6b11e3f2f6afa0c1085e753d80 --- /dev/null +++ b/simpleexample/src/main/java/fxutil/doc/FileMenuController.java @@ -0,0 +1,220 @@ +package fxutil.doc; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.control.Alert; +import javafx.scene.control.Alert.AlertType; +import javafx.scene.control.ButtonType; +import javafx.scene.control.Menu; +import javafx.scene.control.MenuItem; +import javafx.scene.control.TextInputDialog; +import javafx.stage.FileChooser; + +public class FileMenuController { + + private IDocumentStorage<File> documentStorage; + + public void setDocumentStorage(final IDocumentStorage<File> documentStorage) { + this.documentStorage = documentStorage; + if (importMenu != null) { + importMenu.setDisable(documentStorage.getDocumentImporters().isEmpty()); + } + } + + @FXML + public void handleNewAction() { + documentStorage.newDocument(); + } + + private final List<File> recentFiles = new ArrayList<File>(); + + @FXML + private Menu recentMenu; + + protected void updateRecentMenu(final File file) { + recentFiles.remove(file); + recentFiles.add(0, file); + recentMenu.getItems().clear(); + for (final File recentFile : recentFiles) { + final MenuItem menuItem = new MenuItem(); + menuItem.setText(recentFile.toString()); + menuItem.setOnAction(event -> handleOpenAction(event)); + recentMenu.getItems().add(menuItem); + } + } + + private FileChooser fileChooser; + + FileChooser getFileChooser() { + if (fileChooser == null) { + fileChooser = new FileChooser(); + } + return fileChooser; + } + + @FXML + public void handleOpenAction(final ActionEvent event) { + File selection = null; + if (event.getSource() instanceof MenuItem) { + final File file = new File(((MenuItem) event.getSource()).getText()); + if (file.exists()) { + selection = file; + } + } + if (selection == null) { + final FileChooser fileChooser = getFileChooser(); + selection = fileChooser.showOpenDialog(null); + } + if (selection != null) { + handleOpenAction(selection); + } + } + + private void showExceptionDialog(final String message) { + final Alert alert = new Alert(AlertType.ERROR, message, ButtonType.CLOSE); + alert.showAndWait(); + } + + private void showExceptionDialog(final String message, final Exception e) { + showExceptionDialog(message + ": " + e.getLocalizedMessage()); + } + + void handleOpenAction(final File selection) { + try { + documentStorage.openDocument(selection); + updateRecentMenu(selection); + } catch (final IOException e) { + showExceptionDialog("Oops, problem when opening " + selection, e); + } + } + + private void showSaveExceptionDialog(final File location, final Exception e) { + showExceptionDialog("Oops, problem saving to " + location, e); + } + + @FXML + public void handleSaveAction() { + if (documentStorage.getDocumentLocation() == null) { + handleSaveAsAction(); + } else { + try { + documentStorage.saveDocument(); + } catch (final IOException e) { + showSaveExceptionDialog(documentStorage.getDocumentLocation(), e); + } + } + } + + @FXML + public void handleSaveAsAction() { + final FileChooser fileChooser = getFileChooser(); + final File selection = fileChooser.showSaveDialog(null); + handleSaveAsAction(selection); + } + + void handleSaveAsAction(final File selection) { + final File oldStorage = documentStorage.getDocumentLocation(); + try { + documentStorage.setDocumentLocation(selection); + documentStorage.saveDocument(); + } catch (final IOException e) { + showSaveExceptionDialog(documentStorage.getDocumentLocation(), e); + documentStorage.setDocumentLocation(oldStorage); + } + } + + @FXML + public void handleSaveCopyAsAction() { + final FileChooser fileChooser = getFileChooser(); + final File selection = fileChooser.showSaveDialog(null); + handleSaveCopyAsAction(selection); + } + + void handleSaveCopyAsAction(final File selection) { + final File oldStorage = documentStorage.getDocumentLocation(); + try { + documentStorage.setDocumentLocation(selection); + documentStorage.saveDocument(); + } catch (final IOException e) { + showSaveExceptionDialog(selection, e); + } finally { + documentStorage.setDocumentLocation(oldStorage); + } + } + + @FXML + private Menu importMenu; + + @FXML + public void handleFileImportAction() { + final FileChooser fileChooser = getFileChooser(); + final File selection = fileChooser.showOpenDialog(null); + // String path = selection.getPath(); + // int pos = path.lastIndexOf('.'); + // String ext = (pos > 0 ? path.substring(pos + 1) : null); + handleFileImportAction(selection); + } + + void handleFileImportAction(final File selection) { + for (final IDocumentImporter importer : documentStorage.getDocumentImporters()) { + try (InputStream input = new FileInputStream(selection)) { + importer.importDocument(input); + break; + } catch (final Exception e) { + } + } + } + + private TextInputDialog inputDialog; + + @FXML + public void handleURLImportAction() { + if (inputDialog == null) { + inputDialog = new TextInputDialog(); + } + inputDialog.setTitle("Import from URL"); + inputDialog.setHeaderText("Enter URL to import from"); + inputDialog.setContentText("Enter URL: "); + // https://developer.garmin.com/downloads/connect-api/sample_file.gpx + URL url = null; + while (url == null) { + final Optional<String> result = inputDialog.showAndWait(); + if (! result.isPresent()) { + return; + } + try { + url = new URL(result.get()); + if (handleURLImportAction(url)) { + return; + } + url = null; + inputDialog.setHeaderText("Problems reading it..."); + inputDialog.setContentText("Enter another URL: "); + } catch (final MalformedURLException e1) { + inputDialog.setContentText("Enter a valid URL: "); + } + } + } + + boolean handleURLImportAction(final URL url) { + for (final IDocumentImporter importer : documentStorage.getDocumentImporters()) { + try (InputStream input = url.openStream()) { + importer.importDocument(input); + return true; + } catch (final Exception e) { + System.err.println(e.getMessage()); + } + } + return false; + } +} diff --git a/simpleexample/src/main/java/fxutil/doc/IDocumentImporter.java b/simpleexample/src/main/java/fxutil/doc/IDocumentImporter.java new file mode 100644 index 0000000000000000000000000000000000000000..be436c3cbe3f43695088b3c491a82736b933b9a9 --- /dev/null +++ b/simpleexample/src/main/java/fxutil/doc/IDocumentImporter.java @@ -0,0 +1,19 @@ +package fxutil.doc; + +import java.io.IOException; +import java.io.InputStream; + +/** + * An interface with a method for importing domain data from a location. + * The main use is supporting an **import** action in a **File** menu. + * @author hal + * + */ +public interface IDocumentImporter { + /** + * Loads a document from the input stream and sets it as the current document. + * @param inputStream + * @throws IOException + */ + public void importDocument(InputStream inputStream) throws IOException; +} diff --git a/simpleexample/src/main/java/fxutil/doc/IDocumentListener.java b/simpleexample/src/main/java/fxutil/doc/IDocumentListener.java new file mode 100644 index 0000000000000000000000000000000000000000..ec6378199eafaa7d77688ca74586ed0f6867ab99 --- /dev/null +++ b/simpleexample/src/main/java/fxutil/doc/IDocumentListener.java @@ -0,0 +1,18 @@ +package fxutil.doc; + +/** + * Listener interface for the (contents of) the (current) document of an IDocumentStorage, e.g. + * when an **open** action is performed. + * @author hal + * + * @param <D> the document type + * @param <L> the location type + */ +public interface IDocumentListener<D, L> extends IDocumentStorageListener<L> { + /** + * Notifies that the current document has changed. + * @param document the new document + * @param oldDocument the previous document + */ + public void documentChanged(D document, D oldDocument); +} diff --git a/simpleexample/src/main/java/fxutil/doc/IDocumentLoader.java b/simpleexample/src/main/java/fxutil/doc/IDocumentLoader.java new file mode 100644 index 0000000000000000000000000000000000000000..69fef493bc019159108a1874fda5c2f7bd7042c0 --- /dev/null +++ b/simpleexample/src/main/java/fxutil/doc/IDocumentLoader.java @@ -0,0 +1,20 @@ +package fxutil.doc; + +import java.io.InputStream; + +/** + * An interface with a method for loading and returning a document (domain data container) from an InputStream. + * This allows various ways of loading or importing domain data, with different sources and formats. + * @author hal + * + * @param <D> the document type + */ +public interface IDocumentLoader<D> { + /** + * Loads and returns a new document from an InputStream + * @param inputStream + * @return + * @throws Exception + */ + public D loadDocument(InputStream inputStream) throws Exception; +} diff --git a/simpleexample/src/main/java/fxutil/doc/IDocumentPersistence.java b/simpleexample/src/main/java/fxutil/doc/IDocumentPersistence.java new file mode 100644 index 0000000000000000000000000000000000000000..d53b8dd1da4a74fd5aa19688095b2509fbd55ac0 --- /dev/null +++ b/simpleexample/src/main/java/fxutil/doc/IDocumentPersistence.java @@ -0,0 +1,4 @@ +package fxutil.doc; + +public interface IDocumentPersistence<D, L> extends IDocumentLoader<D>, IDocumentSaver<D, L> { +} diff --git a/simpleexample/src/main/java/fxutil/doc/IDocumentSaver.java b/simpleexample/src/main/java/fxutil/doc/IDocumentSaver.java new file mode 100644 index 0000000000000000000000000000000000000000..6754304334e9b9d69b385bbdf04b5f23feafb964 --- /dev/null +++ b/simpleexample/src/main/java/fxutil/doc/IDocumentSaver.java @@ -0,0 +1,19 @@ +package fxutil.doc; + +/** + * An interface with a method for saving a document (domain data container) to a location. + * This allows various ways of saving or exporting domain data, to different locations and formats. + * @author hal + * + * @param <D> the document type + * @param <L> the location type + */ +public interface IDocumentSaver<D, L> { + /** + * Saves the provided document to the provided location + * @param document + * @param documentLocation + * @throws Exception + */ + public void saveDocument(D document, L documentLocation) throws Exception; +} diff --git a/simpleexample/src/main/java/fxutil/doc/IDocumentStorage.java b/simpleexample/src/main/java/fxutil/doc/IDocumentStorage.java new file mode 100644 index 0000000000000000000000000000000000000000..698c15803b1e7e6b127b117d9a54a9515f56b0df --- /dev/null +++ b/simpleexample/src/main/java/fxutil/doc/IDocumentStorage.java @@ -0,0 +1,59 @@ +package fxutil.doc; + +import java.io.IOException; +import java.util.Collection; + +/** + * An interface with the methods necessary for supporting the standard File menu actions. + * The class representing the document (domain data container) is implicit in the implementation of this interface. + * The interface includes methods for getting and setting the location and creating, opening and saving the (current) document. + * @author hal + * + * @param <L> The type of the location, typically java.io.File. + */ +public interface IDocumentStorage<L> { + /** + * Returns the current location (of the current document). + * @return the current location + */ + public L getDocumentLocation(); + + /** + * Sets the current location (of the current document), can be used by a save-as action. + * @param documentLocation + */ + public void setDocumentLocation(L documentLocation); + + /** + * Adds an IDocumentStorageListener that will be notified when the current location changes. + * @param documentStorageListener + */ + + public void addDocumentStorageListener(IDocumentStorageListener<L> documentStorageListener); + /** + * Removes an IDocumentStorageListener. + * @param documentStorageListener + */ + public void removeDocumentStorageListener(IDocumentStorageListener<L> documentStorageListener); + + /** + * Creates a new documents and sets it as the current one, can be used by a new action. + */ + public void newDocument(); + + /** + * Loads a documents from the provided location and sets it as the current one, can be used by an open action. + */ + public void openDocument(L documentLocation) throws IOException; + + /** + * Saves the current document (to the current location), can be used by a save action. + */ + public void saveDocument() throws IOException; + + /** + * Returns the set of IDocumentImporters, can be used by an import action. + * @return + */ + public Collection<IDocumentImporter> getDocumentImporters(); +} diff --git a/simpleexample/src/main/java/fxutil/doc/IDocumentStorageListener.java b/simpleexample/src/main/java/fxutil/doc/IDocumentStorageListener.java new file mode 100644 index 0000000000000000000000000000000000000000..d781f1c39af5c769d4073bd489b02261bda35633 --- /dev/null +++ b/simpleexample/src/main/java/fxutil/doc/IDocumentStorageListener.java @@ -0,0 +1,17 @@ +package fxutil.doc; + +/** + * Listener interface for the (current) location of the (current) document of an IDocumentStorage, e.g. + * when a **save-as** action is performed. + * @author hal + * + * @param <L> + */ +public interface IDocumentStorageListener<L> { + /** + * Notifies that the current document location has changed. + * @param documentLocation the new document location + * @param oldDocumentLocation the previous document location + */ + public void documentLocationChanged(L documentLocation, L oldDocumentLocation); +} diff --git a/simpleexample/src/main/java/fxutil/doc/SimpleJsonFileStorageImpl.java b/simpleexample/src/main/java/fxutil/doc/SimpleJsonFileStorageImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..cf35d0f19ae5d942aebdc3aee3dc709165875d7f --- /dev/null +++ b/simpleexample/src/main/java/fxutil/doc/SimpleJsonFileStorageImpl.java @@ -0,0 +1,65 @@ +package fxutil.doc; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.util.Collection; +import java.util.Collections; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; + +public abstract class SimpleJsonFileStorageImpl<T> extends AbstractDocumentStorageImpl<T, File> implements IDocumentStorage<File> { + + private T document; + + private final ObjectMapper objectMapper; + + private final Class<T> documentClass; + + public SimpleJsonFileStorageImpl(final Class<T> documentClass) { + this.documentClass = documentClass; + final SimpleModule module = new SimpleModule(); + configureJacksonModule(module); + objectMapper = new ObjectMapper(); + objectMapper.registerModule(module); + } + + protected abstract void configureJacksonModule(final SimpleModule module); + + @Override + public T loadDocument(final InputStream inputStream) throws Exception { + return objectMapper.readValue(inputStream, documentClass); + } + + @Override + public void saveDocument(final T document, final File documentLocation) throws Exception { + objectMapper.writeValue(new FileOutputStream(documentLocation, false), document); + } + + @Override + public Collection<IDocumentImporter> getDocumentImporters() { + return Collections.emptyList(); + } + + @Override + public T getDocument() { + return document; + } + + @Override + public void setDocument(final T document) { + final T oldDocument = this.document; + this.document = document; + fireDocumentChanged(oldDocument); + } + + @Override + protected T createDocument() { + try { + return documentClass.getDeclaredConstructor().newInstance(); + } catch (final Exception e) { + throw new RuntimeException("Exception when instantiating " + documentClass, e); + } + } +} diff --git a/simpleexample/src/main/java/simpleex/core/LatLong.java b/simpleexample/src/main/java/simpleex/core/LatLong.java new file mode 100644 index 0000000000000000000000000000000000000000..74fa119dbda10a73cc03acf6054ad9147938db1a --- /dev/null +++ b/simpleexample/src/main/java/simpleex/core/LatLong.java @@ -0,0 +1,118 @@ +package simpleex.core; + +public class LatLong { + + public final double latitude, longitude; + + public LatLong(final double latitude, final double longitude) { + super(); + this.latitude = latitude; + this.longitude = longitude; + } + + public final static String SEPARATOR = ","; + + @Override + public String toString() { + return latitude + SEPARATOR + longitude; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + long temp; + temp = Double.doubleToLongBits(latitude); + result = prime * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(longitude); + result = prime * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final LatLong other = (LatLong) obj; + return (Double.doubleToLongBits(latitude) == Double.doubleToLongBits(other.latitude) && + Double.doubleToLongBits(longitude) == Double.doubleToLongBits(other.longitude)); + } + + public static LatLong valueOf(final String s) { + return valueOf(s, SEPARATOR); + } + + public static LatLong valueOf(final String s, final String sep) { + final int pos = s.indexOf(sep); + if (pos < 0) { + throw new IllegalArgumentException("No '" + sep + "' in " + s); + } + final double lat = Double.valueOf(s.substring(0, pos).trim()); + final double lon = Double.valueOf(s.substring(pos + sep.length()).trim()); + + return new LatLong(lat, lon); + } + + /*::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::*/ + /*:: :*/ + /*:: This routine calculates the distance between two points (given the :*/ + /*:: latitude/longitude of those points). It is being used to calculate :*/ + /*:: the distance between two locations using GeoDataSource (TM) products :*/ + /*:: :*/ + /*:: Definitions: :*/ + /*:: South latitudes are negative, east longitudes are positive :*/ + /*:: :*/ + /*:: Passed to function: :*/ + /*:: lat1, lon1 = Latitude and Longitude of point 1 (in decimal degrees) :*/ + /*:: lat2, lon2 = Latitude and Longitude of point 2 (in decimal degrees) :*/ + /*:: Worldwide cities and other features databases with latitude longitude :*/ + /*:: are available at http://www.geodatasource.com :*/ + /*:: :*/ + /*:: For enquiries, please contact sales@geodatasource.com :*/ + /*:: :*/ + /*:: Official Web site: http://www.geodatasource.com :*/ + /*:: :*/ + /*:: GeoDataSource.com (C) All Rights Reserved 2015 :*/ + /*:: :*/ + /*::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::*/ + public static double distance(final double lat1, final double lon1, final double lat2, final double lon2) { + if (lon1 == lon2 && lat1 == lat2) { + return 0.0; + } + final double theta = lon1 - lon2; + double dist = Math.sin(deg2rad(lat1)) * Math.sin(deg2rad(lat2)) + Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.cos(deg2rad(theta)); + dist = Math.acos(dist); + // convert to degrees + dist = rad2deg(dist); + dist = dist * 60 * 1.1515; + // convert to meters + dist = dist * 1609.344; + return dist; + } + + public static double distance(final LatLong latLong1, final LatLong latLong2) { + return distance(latLong1.latitude, latLong1.longitude, latLong2.latitude, latLong2.longitude); + } + + public double distance(final LatLong latLong2) { + return distance(latitude, longitude, latLong2.latitude, latLong2.longitude); + } + + /*:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::*/ + /*:: This function converts decimal degrees to radians :*/ + /*:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::*/ + private static double deg2rad(final double deg) { + return (deg * Math.PI / 180.0); + } + + /*:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::*/ + /*:: This function converts radians to decimal degrees :*/ + /*:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::*/ + private static double rad2deg(final double rad) { + return (rad * 180 / Math.PI); + } +} diff --git a/simpleexample/src/main/java/simpleex/core/LatLongs.java b/simpleexample/src/main/java/simpleex/core/LatLongs.java new file mode 100644 index 0000000000000000000000000000000000000000..98f383795e9e2720d86d3436e9faffe779f6e8c5 --- /dev/null +++ b/simpleexample/src/main/java/simpleex/core/LatLongs.java @@ -0,0 +1,71 @@ +package simpleex.core; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +public class LatLongs implements Iterable<LatLong> { + + final List<LatLong> latLongs = new ArrayList<>(); + + public LatLongs() { + } + + public LatLongs(final double... latLongsArray) { + addLatLongs(latLongsArray); + } + + public LatLongs(final LatLong... latLongs) { + addLatLongs(latLongs); + } + + public LatLongs(final Collection<LatLong> latLongs) { + addLatLongs(latLongs); + } + + @Override + public Iterator<LatLong> iterator() { + return latLongs.iterator(); + } + + public int getLatLongCount() { + return latLongs.size(); + } + + public LatLong getLatLong(final int num) { + return latLongs.get(num); + } + + public void setLatLong(final int num, final LatLong latLong) { + latLongs.set(num, latLong); + } + + public int addLatLong(final LatLong latLong) { + final int pos = latLongs.size(); + latLongs.add(latLong); + return pos; + } + + public int addLatLongs(final Collection<LatLong> latLongs) { + final int pos = this.latLongs.size(); + this.latLongs.addAll(latLongs); + return pos; + } + + public int addLatLongs(final LatLong... latLongs) { + return addLatLongs(List.of(latLongs)); + } + + public int addLatLongs(final double... latLongsArray) { + final Collection<LatLong> latLongs = new ArrayList<>(latLongsArray.length / 2); + for (int i = 0; i < latLongsArray.length; i += 2) { + latLongs.add(new LatLong(latLongsArray[i], latLongsArray[i + 1])); + } + return addLatLongs(latLongs); + } + + public LatLong removeLatLong(final int num) { + return latLongs.remove(num); + } +} diff --git a/simpleexample/src/main/java/simpleex/core/README.md b/simpleexample/src/main/java/simpleex/core/README.md new file mode 100644 index 0000000000000000000000000000000000000000..834b0e8c9b764b22b851ef9d201a5c6d5bcec881 --- /dev/null +++ b/simpleexample/src/main/java/simpleex/core/README.md @@ -0,0 +1 @@ +# Source package for core code \ No newline at end of file diff --git a/simpleexample/src/main/java/simpleex/json/LatLongDeserializer.java b/simpleexample/src/main/java/simpleex/json/LatLongDeserializer.java new file mode 100644 index 0000000000000000000000000000000000000000..98519525e322e06668faba0c814fc3fdf1f3b955 --- /dev/null +++ b/simpleexample/src/main/java/simpleex/json/LatLongDeserializer.java @@ -0,0 +1,39 @@ +package simpleex.json; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import simpleex.core.LatLong; + +public class LatLongDeserializer extends JsonDeserializer<LatLong> { + + @Override + public LatLong deserialize(final JsonParser jsonParser, final DeserializationContext deserContext) throws IOException, JsonProcessingException { + final JsonNode jsonNode = jsonParser.getCodec().readTree(jsonParser); + return deserialize(jsonNode); + } + + public LatLong deserialize(final JsonNode jsonNode) throws JsonProcessingException { + if (jsonNode instanceof ObjectNode) { + final ObjectNode objectNode = (ObjectNode) jsonNode; + final double latitude = objectNode.get(LatLongSerializer.LATITUDE_FIELD_NAME).asDouble(); + final double longitude = objectNode.get(LatLongSerializer.LONGITUDE_FIELD_NAME).asDouble(); + return new LatLong(latitude, longitude); + } else if (jsonNode instanceof ArrayNode) { + final ArrayNode locationArray = (ArrayNode) jsonNode; + if (locationArray.size() == 2) { + final double latitude = locationArray.get(0).asDouble(); + final double longitude = locationArray.get(1).asDouble(); + return new LatLong(latitude, longitude); + } + } + return null; + } +} diff --git a/simpleexample/src/main/java/simpleex/json/LatLongSerializer.java b/simpleexample/src/main/java/simpleex/json/LatLongSerializer.java new file mode 100644 index 0000000000000000000000000000000000000000..5c939601ab8e83f5095c7511caf38c9b97f32851 --- /dev/null +++ b/simpleexample/src/main/java/simpleex/json/LatLongSerializer.java @@ -0,0 +1,25 @@ +package simpleex.json; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import simpleex.core.LatLong; + +public class LatLongSerializer extends JsonSerializer<LatLong> { + + public static final String LONGITUDE_FIELD_NAME = "longitude"; + public static final String LATITUDE_FIELD_NAME = "latitude"; + + @Override + public void serialize(final LatLong latLon, final JsonGenerator jsonGen, final SerializerProvider provider) throws IOException { + jsonGen.writeStartObject(); + jsonGen.writeFieldName(LATITUDE_FIELD_NAME); + jsonGen.writeNumber(latLon.latitude); + jsonGen.writeFieldName(LONGITUDE_FIELD_NAME); + jsonGen.writeNumber(latLon.longitude); + jsonGen.writeEndObject(); + } +} diff --git a/simpleexample/src/main/java/simpleex/json/LatLongsDeserializer.java b/simpleexample/src/main/java/simpleex/json/LatLongsDeserializer.java new file mode 100644 index 0000000000000000000000000000000000000000..41232de2c15117ea36cfddf97b1e1d230c7e663a --- /dev/null +++ b/simpleexample/src/main/java/simpleex/json/LatLongsDeserializer.java @@ -0,0 +1,35 @@ +package simpleex.json; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; + +import simpleex.core.LatLong; +import simpleex.core.LatLongs; + +public class LatLongsDeserializer extends JsonDeserializer<LatLongs> { + + private final LatLongDeserializer latLongDeserializer = new LatLongDeserializer(); + + @Override + public LatLongs deserialize(final JsonParser jsonParser, final DeserializationContext deserContext) throws IOException, JsonProcessingException { + final JsonNode jsonNode = jsonParser.getCodec().readTree(jsonParser); + if (jsonNode instanceof ArrayNode) { + final ArrayNode latLongsArray = (ArrayNode) jsonNode; + final Collection<LatLong> latLongs = new ArrayList<>(latLongsArray.size()); + for (final JsonNode latLongNode : latLongsArray) { + final LatLong latLong = latLongDeserializer.deserialize(latLongNode); + latLongs.add(latLong); + } + return new LatLongs(latLongs); + } + return null; + } +} diff --git a/simpleexample/src/main/java/simpleex/json/LatLongsSerializer.java b/simpleexample/src/main/java/simpleex/json/LatLongsSerializer.java new file mode 100644 index 0000000000000000000000000000000000000000..d39fec5af1160eeb25dd635f42d86e2294c01090 --- /dev/null +++ b/simpleexample/src/main/java/simpleex/json/LatLongsSerializer.java @@ -0,0 +1,22 @@ +package simpleex.json; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import simpleex.core.LatLong; +import simpleex.core.LatLongs; + +public class LatLongsSerializer extends JsonSerializer<LatLongs> { + + @Override + public void serialize(final LatLongs latLongs, final JsonGenerator jsonGen, final SerializerProvider provider) throws IOException { + jsonGen.writeStartArray(latLongs.getLatLongCount()); + for (final LatLong latLong : latLongs) { + jsonGen.writeObject(latLong); + } + jsonGen.writeEndArray(); + } +} diff --git a/simpleexample/src/main/java/simpleex/json/README.md b/simpleexample/src/main/java/simpleex/json/README.md new file mode 100644 index 0000000000000000000000000000000000000000..eb7378f5a08b201e32c2acac522d060cdc3df28c --- /dev/null +++ b/simpleexample/src/main/java/simpleex/json/README.md @@ -0,0 +1 @@ +# Resources used by the web server code \ No newline at end of file diff --git a/simpleexample/src/main/java/simpleex/ui/DraggableNodeController.java b/simpleexample/src/main/java/simpleex/ui/DraggableNodeController.java new file mode 100644 index 0000000000000000000000000000000000000000..4be472fe2e6386fc17851100881eae0186b457ab --- /dev/null +++ b/simpleexample/src/main/java/simpleex/ui/DraggableNodeController.java @@ -0,0 +1,95 @@ +package simpleex.ui; + +import javafx.event.EventHandler; +import javafx.geometry.Point2D; +import javafx.scene.Node; +import javafx.scene.input.MouseEvent; + +public class DraggableNodeController { + + public DraggableNodeController() { + } + + public DraggableNodeController(final NodeDraggedHandler nodeDraggedHandler) { + setNodeDraggedHandler(nodeDraggedHandler); + } + + private NodeDraggedHandler nodeDraggedHandler; + + public void setNodeDraggedHandler(final NodeDraggedHandler nodeDraggedHandler) { + this.nodeDraggedHandler = nodeDraggedHandler; + } + + private boolean immediate = false; + + public void setImmediate(final boolean immediate) { + this.immediate = immediate; + } + + private Node currentNode = null; + private Point2D startPoint = null; + private Point2D startTranslate = null; + + private final EventHandler<MouseEvent> mousePressedHandler = this::mousePressed; + private final EventHandler<MouseEvent> mouseDraggedHandler = this::mouseDragged; + private final EventHandler<MouseEvent> mouseReleasedHandler = this::mouseReleased; + + public void attach(final Node node) { + node.setOnMousePressed(mousePressedHandler); + node.setOnMouseDragged(mouseDraggedHandler); + node.setOnMouseReleased(mouseReleasedHandler); + } + + public void detach(final Node node) { + node.setOnMousePressed(null); + node.setOnMouseDragged(null); + node.setOnMouseReleased(null); + } + + private void mousePressed(final MouseEvent mouseEvent) { + if (currentNode == null && mouseEvent.getSource() instanceof Node) { + currentNode = (Node) mouseEvent.getSource(); + startPoint = new Point2D(mouseEvent.getSceneX(), mouseEvent.getSceneY()); + startTranslate = new Point2D(currentNode.getTranslateX(), currentNode.getTranslateY()); + mouseEvent.consume(); + } + } + + private void mouseDragged(final MouseEvent mouseEvent) { + if (currentNode != null && currentNode == mouseEvent.getSource()) { + final double dx = mouseEvent.getSceneX() - startPoint.getX(); + final double dy = mouseEvent.getSceneY() - startPoint.getY(); + updateNode(dx, dy); + } + } + + protected void updateNode(final double dx, final double dy) { + if (immediate && nodeDraggedHandler != null) { + nodeDraggedHandler.nodeDragged(currentNode, dx, dy); + startPoint = startPoint.add(dx, dy); + } else { + currentNode.setTranslateX(startTranslate.getX() + dx); + currentNode.setTranslateY(startTranslate.getY() + dy); + } + } + + private void mouseReleased(final MouseEvent mouseEvent) { + if (currentNode != null && currentNode == mouseEvent.getSource()) { + final double dx = mouseEvent.getSceneX() - startPoint.getX(); + final double dy = mouseEvent.getSceneY() - startPoint.getY(); + if (! immediate) { + currentNode.setTranslateX(startTranslate.getX()); + currentNode.setTranslateY(startTranslate.getY()); + } + final Node node = currentNode; + currentNode = null; + if (nodeDraggedHandler != null) { + nodeDraggedHandler.nodeDragged(node, dx, dy); + } + } + } + + public interface NodeDraggedHandler { + public void nodeDragged(Node currentNode2, double dx, double dy); + } +} diff --git a/simpleexample/src/main/java/simpleex/ui/FxApp.java b/simpleexample/src/main/java/simpleex/ui/FxApp.java new file mode 100644 index 0000000000000000000000000000000000000000..5ab81835d07744d84d0c26cc5c0a6dc09e35988f --- /dev/null +++ b/simpleexample/src/main/java/simpleex/ui/FxApp.java @@ -0,0 +1,31 @@ +package simpleex.ui; + +import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.stage.Stage; +import simpleex.core.LatLongs; + +public class FxApp extends Application { + + @Override + public void start(final Stage stage) throws Exception { + final FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("FxApp.fxml")); + final Parent root = fxmlLoader.load(); + final FxAppController controller = fxmlLoader.getController(); + controller.setLatLongs(new LatLongs(63.1, 11.2, 63.2, 11.0)); + final Scene scene = new Scene(root); + stage.setScene(scene); + stage.show(); + } + + public static void main(final String[] args) { + // only needed on ios + System.setProperty("os.target", "ios"); + System.setProperty("os.name", "iOS"); + System.setProperty("glass.platform", "ios"); + System.setProperty("targetos.name", "iOS"); + launch(args); + } +} diff --git a/simpleexample/src/main/java/simpleex/ui/FxAppController.java b/simpleexample/src/main/java/simpleex/ui/FxAppController.java new file mode 100644 index 0000000000000000000000000000000000000000..ab1bc326609a5faf0cd99d8d20453ec4caa62590 --- /dev/null +++ b/simpleexample/src/main/java/simpleex/ui/FxAppController.java @@ -0,0 +1,163 @@ +package simpleex.ui; + +import java.io.File; + +import fxmapcontrol.Location; +import fxmapcontrol.MapBase; +import fxmapcontrol.MapItemsControl; +import fxmapcontrol.MapNode; +import fxmapcontrol.MapProjection; +import fxutil.doc.FileMenuController; +import fxutil.doc.IDocumentListener; +import javafx.collections.FXCollections; +import javafx.fxml.FXML; +import javafx.geometry.Point2D; +import javafx.scene.Node; +import javafx.scene.control.ListView; +import javafx.scene.control.Slider; +import simpleex.core.LatLong; +import simpleex.core.LatLongs; + +public class FxAppController extends FileMenuController implements IDocumentListener<LatLongs, File> { + + private final LatLongsStorage latLongsStorage; + + public FxAppController() { + latLongsStorage = new LatLongsStorage(); + latLongsStorage.addDocumentStorageListener(this); + } + + public LatLongs getLatLongs() { + return latLongsStorage.getDocument(); + } + + // to make it testable + public void setLatLongs(final LatLongs latLongs) { + latLongsStorage.setDocument(latLongs); + updateLocationViewList(0); + } + + // @FXML + // private FileMenuController fileMenuController; + + @FXML + private ListView<LatLong> locationListView; + + @FXML + private MapBase mapView; + + private MapItemsControl<MapNode> markersParent; + private MapMarker marker = null; + private DraggableNodeController draggableMapController = null; + private DraggableNodeController draggableMarkerController = null; + + @FXML + private Slider zoomSlider; + + @FXML + private void initialize() { + // fileMenuController. + setDocumentStorage(latLongsStorage); + // map stuff + // mapView.getChildren().add(MapTileLayer.getOpenStreetMapLayer()); + zoomSlider.valueProperty().addListener((prop, oldValue, newValue) -> mapView.setZoomLevel(zoomSlider.getValue())); + zoomSlider.setValue(8); + markersParent = new MapItemsControl<MapNode>(); + mapView.getChildren().add(markersParent); + draggableMapController = new DraggableNodeController(this::handleMapDragged); + draggableMapController.setImmediate(true); + draggableMapController.attach(mapView); + draggableMarkerController = new DraggableNodeController(this::handleMarkerDragged); + // the location list + locationListView.getSelectionModel().selectedIndexProperty().addListener((prop, oldValue, newValue) -> updateMapMarker(true)); + } + + private void handleMapDragged(final Node node, final double dx, final double dy) { + final MapProjection projection = mapView.getProjection(); + final Point2D point = projection.locationToViewportPoint(mapView.getCenter()); + final Location newCenter = projection.viewportPointToLocation(point.add(-dx, -dy)); + mapView.setCenter(newCenter); + } + + private void handleMarkerDragged(final Node node, final double dx, final double dy) { + final MapProjection projection = mapView.getProjection(); + final Point2D point = projection.locationToViewportPoint(marker.getLocation()); + final Location newLocation = projection.viewportPointToLocation(point.add(dx, dy)); + getLatLongs().setLatLong(locationListView.getSelectionModel().getSelectedIndex(), location2LatLong(newLocation)); + updateLocationViewListSelection(false); + } + + private LatLong location2LatLong(final Location newLocation) { + return new LatLong(newLocation.getLatitude(), newLocation.getLongitude()); + } + + private void updateMapMarker(final boolean centerOnMarker) { + final int num = locationListView.getSelectionModel().getSelectedIndex(); + if (num < 0 || num >= getLatLongs().getLatLongCount()) { + markersParent.getItems().clear(); + if (draggableMarkerController != null) { + draggableMarkerController.detach(marker); + } + marker = null; + } else { + final LatLong latLong = getLatLongs().getLatLong(num); + if (marker == null) { + marker = new MapMarker(latLong); + markersParent.getItems().add(marker); + if (draggableMarkerController != null) { + draggableMarkerController.attach(marker); + } + } else { + marker.setLocation(latLong); + } + if (centerOnMarker) { + mapView.setCenter(marker.getLocation()); + } + } + } + + @FXML + private void handleAddLocation() { + final Location center = mapView.getCenter(); + final int pos = getLatLongs().addLatLong(location2LatLong(center)); + updateLocationViewList(pos); + } + + private void updateLocationViewListSelection(final Boolean updateMapMarker) { + final int selectedIndex = locationListView.getSelectionModel().getSelectedIndex(); + updateLocationViewListItem(selectedIndex); + if (updateMapMarker != null) { + updateMapMarker(updateMapMarker); + } + } + + private void updateLocationViewListItem(final int index) { + locationListView.getItems().set(index, getLatLongs().getLatLong(index)); + } + + private void updateLocationViewList(int selectedIndex) { + final LatLong[] latLongs = new LatLong[getLatLongs().getLatLongCount()]; + for (int i = 0; i < latLongs.length; i++) { + latLongs[i] = getLatLongs().getLatLong(i); + } + final int oldSelectionIndex = locationListView.getSelectionModel().getSelectedIndex(); + locationListView.setItems(FXCollections.observableArrayList(latLongs)); + if (selectedIndex < 0 || selectedIndex >= latLongs.length) { + selectedIndex = oldSelectionIndex; + } + if (selectedIndex >= 0 && selectedIndex < getLatLongs().getLatLongCount()) { + locationListView.getSelectionModel().select(selectedIndex); + } + } + + // IDocumentListener + + @Override + public void documentLocationChanged(final File documentLocation, final File oldDocumentLocation) { + } + + @Override + public void documentChanged(final LatLongs document, final LatLongs oldDocument) { + updateLocationViewList(0); + } +} diff --git a/simpleexample/src/main/java/simpleex/ui/LatLongsApp.java b/simpleexample/src/main/java/simpleex/ui/LatLongsApp.java new file mode 100644 index 0000000000000000000000000000000000000000..6436b5d21cc9db32334d5f4055ffe61847d4db88 --- /dev/null +++ b/simpleexample/src/main/java/simpleex/ui/LatLongsApp.java @@ -0,0 +1,98 @@ +package simpleex.ui; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.Collection; +import java.util.stream.Collectors; + +import fxutil.doc.AbstractDocumentStorageImpl; +import fxutil.doc.IDocumentImporter; +import fxutil.doc.IDocumentLoader; +import fxutil.doc.IDocumentPersistence; +import fxutil.doc.IDocumentStorage; +import simpleex.core.LatLongs; + +public class LatLongsApp { + + private LatLongs latLongs = null; + + private final IDocumentPersistence<LatLongs, File> documentPersistence = new IDocumentPersistence<LatLongs, File>() { + + @Override + public LatLongs loadDocument(final InputStream inputStream) throws Exception { + // TODO + return null; + } + + @Override + public void saveDocument(final LatLongs document, final File documentLocation) throws Exception { + try (OutputStream output = new FileOutputStream(documentLocation)) { + // TODO + } + } + }; + + private final AbstractDocumentStorageImpl<LatLongs, File> documentStorage = new AbstractDocumentStorageImpl<LatLongs, File>() { + + @Override + protected LatLongs getDocument() { + return latLongs; + } + + @Override + protected void setDocument(final LatLongs document) { + final LatLongs oldDocument = getDocument(); + LatLongsApp.this.latLongs = document; + fireDocumentChanged(oldDocument); + } + + @Override + protected LatLongs createDocument() { + return new LatLongs(); + } + + @Override + protected InputStream toInputStream(final File storage) throws IOException { + return new FileInputStream(storage); + } + + @Override + public LatLongs loadDocument(final InputStream inputStream) throws Exception { + return documentPersistence.loadDocument(inputStream); + } + + @Override + public void saveDocument(final LatLongs document, final File documentLocation) throws Exception { + documentPersistence.saveDocument(document, documentLocation); + } + + @Override + public Collection<IDocumentImporter> getDocumentImporters() { + return documentLoaders.stream().map(loader -> new IDocumentImporter() { + @Override + public void importDocument(final InputStream inputStream) throws IOException { + try { + setDocumentAndLocation(loader.loadDocument(inputStream), null); + } catch (final Exception e) { + throw new IOException(e); + } + } + }).collect(Collectors.toList()); + } + }; + + public IDocumentStorage<File> getDocumentStorage() { + return documentStorage; + } + + private final Collection<IDocumentLoader<LatLongs>> documentLoaders = Arrays.asList(); + + public Iterable<IDocumentLoader<LatLongs>> getDocumentLoaders() { + return documentLoaders; + } +} diff --git a/simpleexample/src/main/java/simpleex/ui/LatLongsStorage.java b/simpleexample/src/main/java/simpleex/ui/LatLongsStorage.java new file mode 100644 index 0000000000000000000000000000000000000000..53e108bf799ae1f70edecada4d0d7e27703b3a6b --- /dev/null +++ b/simpleexample/src/main/java/simpleex/ui/LatLongsStorage.java @@ -0,0 +1,29 @@ +package simpleex.ui; + +import java.io.File; + +import com.fasterxml.jackson.databind.module.SimpleModule; + +import fxutil.doc.IDocumentStorage; +import fxutil.doc.SimpleJsonFileStorageImpl; +import simpleex.core.LatLong; +import simpleex.core.LatLongs; +import simpleex.json.LatLongDeserializer; +import simpleex.json.LatLongSerializer; +import simpleex.json.LatLongsDeserializer; +import simpleex.json.LatLongsSerializer; + +public class LatLongsStorage extends SimpleJsonFileStorageImpl<LatLongs> implements IDocumentStorage<File> { + + public LatLongsStorage() { + super(LatLongs.class); + } + + @Override + protected void configureJacksonModule(final SimpleModule module) { + module.addSerializer(LatLong.class, new LatLongSerializer()); + module.addSerializer(LatLongs.class, new LatLongsSerializer()); + module.addDeserializer(LatLong.class, new LatLongDeserializer()); + module.addDeserializer(LatLongs.class, new LatLongsDeserializer()); + } +} diff --git a/simpleexample/src/main/java/simpleex/ui/MapMarker.java b/simpleexample/src/main/java/simpleex/ui/MapMarker.java new file mode 100644 index 0000000000000000000000000000000000000000..f61ce55782dafe2da7155672778dccd12495eb25 --- /dev/null +++ b/simpleexample/src/main/java/simpleex/ui/MapMarker.java @@ -0,0 +1,22 @@ +package simpleex.ui; + +import fxmapcontrol.Location; +import fxmapcontrol.MapItem; +import javafx.scene.paint.Color; +import javafx.scene.shape.Circle; +import simpleex.core.LatLong; + +public class MapMarker extends MapItem<LatLong> { + + public MapMarker(final LatLong latLong) { + setLocation(latLong); + final Circle circle = new Circle(); + circle.setRadius(5); + circle.setFill(Color.BLUE); + getChildren().add(circle); + } + + public void setLocation(final LatLong latLong) { + setLocation(new Location(latLong.latitude, latLong.longitude)); + } +} diff --git a/simpleexample/src/main/java/simpleex/ui/README.md b/simpleexample/src/main/java/simpleex/ui/README.md new file mode 100644 index 0000000000000000000000000000000000000000..9b8ca7943de735a79f4661ae89a93f8aac366bc6 --- /dev/null +++ b/simpleexample/src/main/java/simpleex/ui/README.md @@ -0,0 +1 @@ +# The UI source code, a minimal JavaFX/FXML UI \ No newline at end of file diff --git a/simpleexample/src/main/resources/simpleex/ui/FileMenu.fxml b/simpleexample/src/main/resources/simpleex/ui/FileMenu.fxml new file mode 100644 index 0000000000000000000000000000000000000000..ffaf61d6c0e0da7bdfce6576780eef9508af61b4 --- /dev/null +++ b/simpleexample/src/main/resources/simpleex/ui/FileMenu.fxml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import java.lang.*?> +<?import javafx.scene.control.Menu?> +<?import javafx.scene.control.MenuItem?> +<?import javafx.scene.control.SeparatorMenuItem?> + +<Menu xmlns:fx="http://javafx.com/fxml" text="File" fx:controller="fxutil.doc.FileMenuController"> + <items> + <MenuItem text="New" accelerator="Meta+N" onAction="#handleNewAction"/> + <MenuItem text="Open..." accelerator="Meta+O" onAction="#handleOpenAction"/> + <Menu fx:id="recentMenu" text="Open Recent"/> + <SeparatorMenuItem/> + <MenuItem text="Save" accelerator="Meta+S" onAction="#handleSaveAction"/> + <MenuItem text="Save As..." onAction="#handleSaveAsAction"/> + <MenuItem text="Save Copy As..." onAction="#handleSaveCopyAsAction"/> + <SeparatorMenuItem/> + <Menu fx:id="importMenu" text="Import"> + <MenuItem text="File..." onAction="#handleFileImportAction"/> + <MenuItem text="URL..." onAction="#handleURLImportAction"/> + </Menu> + </items> +</Menu> diff --git a/simpleexample/src/main/resources/simpleex/ui/FxApp.fxml b/simpleexample/src/main/resources/simpleex/ui/FxApp.fxml new file mode 100644 index 0000000000000000000000000000000000000000..a2410ddb586006c8325e07568bdb2bb3d379902a --- /dev/null +++ b/simpleexample/src/main/resources/simpleex/ui/FxApp.fxml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import javafx.scene.layout.BorderPane?> +<?import javafx.scene.layout.VBox?> +<?import javafx.scene.control.ListView?> +<?import javafx.scene.control.Slider?> +<?import fxmapcontrol.MapBase?> +<?import javafx.scene.control.Button?> +<?import javafx.scene.layout.HBox?> +<?import javafx.scene.control.TextField?> +<?import javafx.scene.control.Label?> +<?import fxmapcontrol.MapTileLayer?> +<?import fxmapcontrol.TileSource?> +<?import javafx.scene.control.MenuBar?> + +<?import javafx.scene.control.Menu?> +<?import javafx.scene.control.MenuItem?> +<?import javafx.scene.control.SeparatorMenuItem?> + +<BorderPane xmlns:fx="http://javafx.com/fxml" + fx:controller="simpleex.ui.FxAppController" + prefHeight="750" prefWidth="1000"> + <top> + <VBox> + <MenuBar > + <menus> + <fx:include fx:id="fileMenu" source="FileMenu.fxml"/> + </menus> + </MenuBar> + </VBox> + </top> + <left> + <VBox fillWidth="true"> + <ListView fx:id="locationListView"/> + <Button text="Add location" onAction="#handleAddLocation"/> + </VBox> + </left> + <center> + <VBox> + <MapBase fx:id="mapView"> + <MapTileLayer name="OpenStreetMap" minZoomLevel="0" maxZoomLevel="17"> + <tileSource> + <TileSource urlFormat="http://a.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png"/> + <!-- + <TileSource urlFormat="https://{c}.tile.openstreetmap.org/{z}/{x}/{y}.png"/> + <TileSource urlFormat="http://opencache.statkart.no/gatekeeper/gk/gk.open_gmaps?layers=topo2&zoom={z}&x={x}&y={y}"/> + <TileSource urlFormat="http://mt1.google.com/vt/lyrs=m@129&hl=en&s=Galileo&z={z}&x={x}&y={y}"/> + --> + </tileSource> + </MapTileLayer> + </MapBase> + <Slider fx:id="zoomSlider" min="1" max="20" value="9"/> + </VBox> + </center> +</BorderPane> diff --git a/simpleexample/src/test/java/simpleex/core/LatLongTest.java b/simpleexample/src/test/java/simpleex/core/LatLongTest.java new file mode 100644 index 0000000000000000000000000000000000000000..b6a333546f6e2a2ec5c537207243bd211edcda3e --- /dev/null +++ b/simpleexample/src/test/java/simpleex/core/LatLongTest.java @@ -0,0 +1,32 @@ +package simpleex.core; + +import org.junit.Assert; +import org.junit.Test; + +public class LatLongTest { + + @Test + public void testToString() { + final LatLong latLong = new LatLong(63.0, 10.0); + Assert.assertEquals(Double.toString(63) + "," + Double.toString(10), latLong.toString()); + } + + @Test + public void testValueOf() { + testLatLong(LatLong.valueOf("63.0, 10.0"), 63.0, 10.0); + testLatLong(LatLong.valueOf("63.0, 10.0", ","), 63.0, 10.0); + testLatLong(LatLong.valueOf("63.0; 10.0", ";"), 63.0, 10.0); + } + + private void testLatLong(final LatLong latLong, final double lat, final double lon) { + Assert.assertEquals(lat, latLong.latitude, 0.0); + Assert.assertEquals(lon, latLong.longitude, 0.0); + } + + @Test + public void testEquals() { + Assert.assertTrue(new LatLong(63.0, 10.0).equals(new LatLong(63.0, 10.0))); + Assert.assertFalse(new LatLong(10.0, 63.0).equals(new LatLong(63.0, 10.0))); + Assert.assertFalse(new LatLong(10.0, 63.0).equals(null)); + } +} diff --git a/simpleexample/src/test/java/simpleex/core/LatLongsTest.java b/simpleexample/src/test/java/simpleex/core/LatLongsTest.java new file mode 100644 index 0000000000000000000000000000000000000000..0c7d4f2b5b3539d55682ed0ba2fe8b42f2fcfdc2 --- /dev/null +++ b/simpleexample/src/test/java/simpleex/core/LatLongsTest.java @@ -0,0 +1,22 @@ +package simpleex.core; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import simpleex.core.LatLongs; + +public class LatLongsTest { + + private LatLongs latLongs; + + @Before + public void setup() { + latLongs = new LatLongs(); + } + + @Test + public void testEmptyConstructor() { + Assert.assertEquals(0, latLongs.getLatLongCount()); + } +} diff --git a/simpleexample/src/test/java/simpleex/json/AbstractJsonTest.java b/simpleexample/src/test/java/simpleex/json/AbstractJsonTest.java new file mode 100644 index 0000000000000000000000000000000000000000..cfbd5e65d7b90577bd4c3e16b436ac270ecaa630 --- /dev/null +++ b/simpleexample/src/test/java/simpleex/json/AbstractJsonTest.java @@ -0,0 +1,49 @@ +package simpleex.json; + +import java.io.IOException; + +import org.junit.Assert; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; + +public abstract class AbstractJsonTest { + + private ObjectMapper objectMapper; + + public ObjectMapper getObjectMapper() { + return objectMapper; + } + + public void setup() { + objectMapper = createObjectMapper(); + } + + protected abstract ObjectMapper createObjectMapper(); + + protected <T> SimpleModule createSimpleModule(final Class<T> clazz, final JsonSerializer<T> serializer, final JsonDeserializer<T> deserializer) { + return new SimpleModule() + .addSerializer(clazz, serializer) + .addDeserializer(clazz, deserializer); + } + + protected <T> ObjectMapper createObjectMapper(final Class<T> clazz, final JsonSerializer<T> serializer, final JsonDeserializer<T> deserializer) { + return new ObjectMapper().registerModule(createSimpleModule(clazz, serializer, deserializer)); + } + + protected void assertEqualsIgnoreWhitespace(final String expected, final String actual) throws Exception { + Assert.assertEquals(expected, actual.replaceAll("\\s+", "")); + } + + protected void assertWriteValue(final String expected, final Object value) throws Exception { + assertEqualsIgnoreWhitespace(expected, getObjectMapper().writeValueAsString(value)); + } + + protected <T> T readValue(final String s, final Class<T> clazz) throws IOException, JsonParseException, JsonMappingException { + return getObjectMapper().readValue(s, clazz); + } +} diff --git a/simpleexample/src/test/java/simpleex/json/LatLongJsonTest.java b/simpleexample/src/test/java/simpleex/json/LatLongJsonTest.java new file mode 100644 index 0000000000000000000000000000000000000000..7e926a56687dfec28212631ab250a4708e49d4ef --- /dev/null +++ b/simpleexample/src/test/java/simpleex/json/LatLongJsonTest.java @@ -0,0 +1,35 @@ +package simpleex.json; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import simpleex.core.LatLong; +import simpleex.json.LatLongDeserializer; +import simpleex.json.LatLongSerializer; + +public class LatLongJsonTest extends AbstractJsonTest { + + @Before + @Override + public void setup() { + super.setup(); + } + + @Override + protected ObjectMapper createObjectMapper() { + return createObjectMapper(LatLong.class, new LatLongSerializer(), new LatLongDeserializer()); + } + + @Test + public void testLatLongSerialization() throws Exception { + assertWriteValue("{\"latitude\":63.1,\"longitude\":12.3}", new LatLong(63.1, 12.3)); + } + + @Test + public void testLatLongDeserialization() throws Exception { + Assert.assertEquals(new LatLong(63.1, 12.3), readValue("[ 63.1, 12.3 ]", LatLong.class)); + } +} diff --git a/simpleexample/src/test/java/simpleex/ui/FxAppTest.java b/simpleexample/src/test/java/simpleex/ui/FxAppTest.java new file mode 100644 index 0000000000000000000000000000000000000000..fad97d30c37c873b035c4a6802f5de349d519d35 --- /dev/null +++ b/simpleexample/src/test/java/simpleex/ui/FxAppTest.java @@ -0,0 +1,109 @@ +package simpleex.ui; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.testfx.framework.junit.ApplicationTest; + +import fxmapcontrol.Location; +import fxmapcontrol.MapBase; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.ListView; +import javafx.stage.Stage; +import simpleex.core.LatLong; +import simpleex.core.LatLongs; + +public class FxAppTest extends ApplicationTest { + + @BeforeClass + public static void headless() { + if (Boolean.valueOf(System.getProperty("gitlab-ci", "false"))) { + System.setProperty("prism.verbose", "true"); // optional + System.setProperty("java.awt.headless", "true"); + System.setProperty("testfx.robot", "glass"); + System.setProperty("testfx.headless", "true"); + System.setProperty("glass.platform", "Monocle"); + System.setProperty("monocle.platform", "Headless"); + System.setProperty("prism.order", "sw"); + System.setProperty("prism.text", "t2k"); + System.setProperty("testfx.setup.timeout", "2500"); + } + } + + private FxAppController controller; + private LatLongs latLongs; + + @Override + public void start(final Stage stage) throws Exception { + final FXMLLoader loader = new FXMLLoader(getClass().getResource("FxApp.fxml")); + final Parent root = loader.load(); + this.controller = loader.getController(); + setUpLatLongs(); + final Scene scene = new Scene(root); + stage.setScene(scene); + stage.show(); + } + + private List<LatLong> latLongList; + + private void setUpLatLongs() { + // test data + latLongList = new ArrayList<>(List.of(new LatLong(63.1, 11.2), new LatLong(63.2, 11.0))); + // "mocked" (faked) LatLongs object with very specific and limited behavior + latLongs = mock(LatLongs.class); + // get nth LatLong object + when(latLongs.getLatLong(anyInt())).then(invocation -> latLongList.get(invocation.getArgument(0))); + // get the number of LatLong objects + when(latLongs.getLatLongCount()).then(invocation -> latLongList.size()); + // iterator for LatLong objects + when(latLongs.iterator()).then(invocation -> latLongList.iterator()); + controller.setLatLongs(latLongs); + } + + @Test + public void testController() { + Assert.assertTrue(this.controller instanceof FxAppController); + } + + @Test + public void testLocationListView() { + final ListView<?> locationListView = lookup("#locationListView").query(); + // list contains equals elements in same order + Assert.assertEquals(latLongList, locationListView.getItems()); + // first list element is auto-selected + Assert.assertEquals(0, locationListView.getSelectionModel().getSelectedIndex()); + } + + @Test + public void testMapView() { + final MapBase mapView = lookup("#mapView").query(); + // center of map view is approx. the first LatLong object + final Location center = mapView.getCenter(); + final double epsilon = 0.000001; // round-off error + Assert.assertEquals(latLongList.get(0).latitude, center.getLatitude(), epsilon); + Assert.assertEquals(latLongList.get(0).longitude, center.getLongitude(), epsilon); + } + + @Test + public void testAddLocation() { + // needs map center + final Location center = ((MapBase) lookup("#mapView").query()).getCenter(); + // add behavior for add + final LatLong latLong = new LatLong(center.getLatitude(), center.getLongitude()); + when(latLongs.addLatLong(latLong)).thenReturn(2); // add center + + // make test less sensitive to exact button text + final Button addLocButton = lookup(node -> node instanceof Button && ((Button) node).getText().toLowerCase().startsWith("add loc")).query(); + clickOn(addLocButton); + } +} diff --git a/simpleexample/src/test/resources/simpleex/ui/FileMenu.fxml b/simpleexample/src/test/resources/simpleex/ui/FileMenu.fxml new file mode 100644 index 0000000000000000000000000000000000000000..ffaf61d6c0e0da7bdfce6576780eef9508af61b4 --- /dev/null +++ b/simpleexample/src/test/resources/simpleex/ui/FileMenu.fxml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import java.lang.*?> +<?import javafx.scene.control.Menu?> +<?import javafx.scene.control.MenuItem?> +<?import javafx.scene.control.SeparatorMenuItem?> + +<Menu xmlns:fx="http://javafx.com/fxml" text="File" fx:controller="fxutil.doc.FileMenuController"> + <items> + <MenuItem text="New" accelerator="Meta+N" onAction="#handleNewAction"/> + <MenuItem text="Open..." accelerator="Meta+O" onAction="#handleOpenAction"/> + <Menu fx:id="recentMenu" text="Open Recent"/> + <SeparatorMenuItem/> + <MenuItem text="Save" accelerator="Meta+S" onAction="#handleSaveAction"/> + <MenuItem text="Save As..." onAction="#handleSaveAsAction"/> + <MenuItem text="Save Copy As..." onAction="#handleSaveCopyAsAction"/> + <SeparatorMenuItem/> + <Menu fx:id="importMenu" text="Import"> + <MenuItem text="File..." onAction="#handleFileImportAction"/> + <MenuItem text="URL..." onAction="#handleURLImportAction"/> + </Menu> + </items> +</Menu> diff --git a/simpleexample/src/test/resources/simpleex/ui/FxApp.fxml b/simpleexample/src/test/resources/simpleex/ui/FxApp.fxml new file mode 100644 index 0000000000000000000000000000000000000000..a2410ddb586006c8325e07568bdb2bb3d379902a --- /dev/null +++ b/simpleexample/src/test/resources/simpleex/ui/FxApp.fxml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import javafx.scene.layout.BorderPane?> +<?import javafx.scene.layout.VBox?> +<?import javafx.scene.control.ListView?> +<?import javafx.scene.control.Slider?> +<?import fxmapcontrol.MapBase?> +<?import javafx.scene.control.Button?> +<?import javafx.scene.layout.HBox?> +<?import javafx.scene.control.TextField?> +<?import javafx.scene.control.Label?> +<?import fxmapcontrol.MapTileLayer?> +<?import fxmapcontrol.TileSource?> +<?import javafx.scene.control.MenuBar?> + +<?import javafx.scene.control.Menu?> +<?import javafx.scene.control.MenuItem?> +<?import javafx.scene.control.SeparatorMenuItem?> + +<BorderPane xmlns:fx="http://javafx.com/fxml" + fx:controller="simpleex.ui.FxAppController" + prefHeight="750" prefWidth="1000"> + <top> + <VBox> + <MenuBar > + <menus> + <fx:include fx:id="fileMenu" source="FileMenu.fxml"/> + </menus> + </MenuBar> + </VBox> + </top> + <left> + <VBox fillWidth="true"> + <ListView fx:id="locationListView"/> + <Button text="Add location" onAction="#handleAddLocation"/> + </VBox> + </left> + <center> + <VBox> + <MapBase fx:id="mapView"> + <MapTileLayer name="OpenStreetMap" minZoomLevel="0" maxZoomLevel="17"> + <tileSource> + <TileSource urlFormat="http://a.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png"/> + <!-- + <TileSource urlFormat="https://{c}.tile.openstreetmap.org/{z}/{x}/{y}.png"/> + <TileSource urlFormat="http://opencache.statkart.no/gatekeeper/gk/gk.open_gmaps?layers=topo2&zoom={z}&x={x}&y={y}"/> + <TileSource urlFormat="http://mt1.google.com/vt/lyrs=m@129&hl=en&s=Galileo&z={z}&x={x}&y={y}"/> + --> + </tileSource> + </MapTileLayer> + </MapBase> + <Slider fx:id="zoomSlider" min="1" max="20" value="9"/> + </VBox> + </center> +</BorderPane>