I feel the passage of time in Emacs-Qiita I felt that the idea was very good, but unfortunately I was not an Emacs user, so when I couldn't feel the flow of.
In order to feel the passage of time, I created a desktop accessory-like application in Java, which I'm used to. I have received permission from the original author, @zk_phi, to publish it, so I would like to introduce it here.
You can get the pre-built jar file (littlesky.jar
) from here.
The source code is posted on GitHub> https://github.com/opengl-8080/little-sky
OS I have confirmed that it works on Windows 10.
I don't have a Mac environment at hand, so I'm not sure if it will work on a Mac. I'm not going to write OS-dependent processing, but if it doesn't work on Mac, please pull request.
JRE It should work with Java 8u40 and above (because you are using Dialog) .. I saw that it works with Java 9 as well.
$ java -jar littlesky.jar
The configuration file is saved in the current directory at runtime (littlesky.xml
).
The second and subsequent startups refer to the configuration file in the current directory. If you want to change the startup location, move the configuration file as well.
At the first startup, a dialog for entering location information is displayed.
Enter the latitude (Latitude
) and longitude (Longitude
).
(Used to obtain sunrise / sunset times and weather information)
As with the original, the sky icon (sky icon), time, and temperature information are displayed. The sky icon displays the moon phases when it is sunny, and the weather icons when it is raining or snowing.
The background color changes according to the time.
Right-click on the time to display the context menu. You can close the application and change the settings.
As with the original, you can use the weather function using OpenWeatherMap.
Get your API key in OpenWeatherMap.
The API key can be obtained by registering an account on OpenWeatherMap. If you search for "OpenWeatherMap API key", you will find a lot of information.
Select Options from the context menu to display a form for entering the API key.
Enter the API key you obtained in [API Key] and click the [Save] button to save the settings.
After entering the API key, you will be able to select Weather service> Start from the context menu. Select this to activate the weather function.
Once every 15 minutes, the weather, cloud volume, and temperature information will be acquired based on the set location information and reflected in the display.
Once you set the API key, the weather function will start automatically the next time you start the application.
If you want to stop the weather function, select Weather service> Stop from the context menu or empty the API key and save.
In an environment where a proxy is required to access the Internet, proxy settings are required.
Open the settings dialog with Options in the context menu.
In HTTP Proxy, enter the proxy settings.
If no port is entered, 80
is used by default.
Enter the username and password only if the proxy requires authentication.
littkesky.xml
).You can change the display settings from [View] in the context menu.
If you check [Always on top], the window will always be in the foreground of the desktop. The default is off.
By switching the check of [Show seconds], you can show / hide the seconds of the time. The default is on.
By switching the [Show temperature] check, you can show / hide the temperature. The default is on.
By switching the check of [Show sky status icon], you can switch the display / non-display of the sky icon (moon phase / weather icon). The default is on.
I was pointed out by Bukome, and I thought it was a little bad, so I'm sorry to add it later, but I would like to summarize what I learned while making this application.
package sample.javafx;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
public class NoFrameWindow extends Application {
public static void main(String[] args) {
launch(NoFrameWindow.class, args);
}
@Override
public void start(Stage primaryStage) throws Exception {
Pane pane = new Pane();
pane.setPrefWidth(200);
pane.setPrefHeight(100);
pane.setStyle("-fx-background-radius: 50; -fx-background-color: yellow;");
Scene scene = new Scene(pane);
scene.setFill(Color.TRANSPARENT);
primaryStage.initStyle(StageStyle.TRANSPARENT);
primaryStage.setScene(scene);
primaryStage.show();
}
}
Execution result
Description
--To create a window without a window frame, first specify StageStyle.TRANSPARENT
in style
of Stage
.
--This will make the window frame of the window disappear
--However, with this alone, the background of Scene
is not transparent.
--If Scene
is not transparent, even if you round the corners of the node, it will remain square in appearance.
--Set Color.TRANSPARENT
withsetFill ()
to make Scene
transparent
--Then, if you round the corners of the node registered in Scene
, you can create a window without a window frame of any shape as shown in ↑.
--The rounded corners of the background can be specified with -fx-background-radius
in CSS.
--If you set it in the implementation, it will probably look like ↓
Set the background programmatically
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.paint.Color;
...
BackgroundFill backgroundFill = new BackgroundFill(Color.YELLOW, new CornerRadii(50), null);
Background background = new Background(backgroundFill);
pane.setBackground(background);
If you remove the window frame, you cannot drag the window as it is. This implementation requires you to do your best on your own.
There are some similar questions when looking for Stackoverflow, and the implementation method is also introduced, so I will use it as it is.
Moving an undecorated stage in javafx 2 - Stack Overflow
package sample.javafx;
...
public class NoFrameWindow extends Application {
...
private double mouseX;
private double mouseY;
@Override
public void start(Stage primaryStage) throws Exception {
Pane pane = new Pane();
...
pane.setOnMousePressed(e -> {
this.mouseX = primaryStage.getX() - e.getScreenX();
this.mouseY = primaryStage.getY() - e.getScreenY();
});
pane.setOnMouseDragged(e -> {
primaryStage.setX(e.getScreenX() + this.mouseX);
primaryStage.setY(e.getScreenY() + this.mouseY);
});
...
}
}
Execution result
As for the background color of the sky, the background color is decided only for some key times (referring to the original implementation method). The color between a certain key time A and key time B interpolates the color between the two colors A and B.
Color interpolation between two colors is directly applied to the Color
class, which is a JavaFX color class, as it is [interpolate (Color, double)](https://docs.oracle.com/javase/jp/8/javafx/ There is a method called api / javafx / scene / paint / Color.html # interpolate-javafx.scene.paint.Color-double-), and I used it.
ʻInterpolate ()calculates the color at the position of the ratio (value from
0.0 to
1.0) specified by the second argument from the color that becomes the receiver to the color specified by the first argument. return. For example,
color1.interporate (color2, 0.5)will calculate and return a value that is exactly between
color1 and
color2 (
0.5`).
The ratio is from the time set for each key and the current time [Duration] of Date & Time API (https://docs.oracle.com/javase/jp/8/docs/api/java/time/Duration.html) It was calculated using. The specific implementation is in the SkyColorGradation class.
Recently [I studied JavaFX hard](https://qiita.com/opengl-8080/items/51bef25aa05ecd929a3d#javafx-%E3%83%97%E3%83%AD%E3%83%91%E3%83% 86% E3% 82% A3) In this implementation, I tried to actively use the JavaFX properties I learned at that time.
Taking advantage of the fact that the observer pattern can be implemented simply by using JavaFX properties, the overall configuration looks like ↓.
For example, the time display is as follows.
model
ClockBase.java
package littlesky.model.clock;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import java.time.LocalDate;
import java.time.LocalTime;
public class ClockBase implements Clock {
protected final ReadOnlyObjectWrapper<LocalDate> date = new ReadOnlyObjectWrapper<>();
protected final ReadOnlyObjectWrapper<LocalTime> time = new ReadOnlyObjectWrapper<>();
@Override
public LocalDate getDate() {
return this.date.get();
}
@Override
public LocalTime getTime() {
return this.time.get();
}
@Override
public ReadOnlyObjectProperty<LocalDate> dateProperty() {
return this.date.getReadOnlyProperty();
}
@Override
public ReadOnlyObjectProperty<LocalTime> timeProperty() {
return this.time.getReadOnlyProperty();
}
}
ReadTimeClock.java
package littlesky.model.clock;
import javafx.application.Platform;
import java.time.LocalDateTime;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class RealTimeClock extends ClockBase {
public RealTimeClock() {
this.updateDateTime();
}
public void start() {
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor((runnable) -> {
Thread thread = new Thread(runnable);
thread.setDaemon(true);
return thread;
});
executor.scheduleAtFixedRate(() -> {
Platform.runLater(this::updateDateTime);
}, 0, 500, TimeUnit.MILLISECONDS);
}
private void updateDateTime() {
LocalDateTime now = LocalDateTime.now();
if (this.getDate() == null || !this.getDate().equals(now.toLocalDate())) {
this.date.set(now.toLocalDate());
}
this.time.set(now.toLocalTime());
}
}
This class counts the current time. It spawns a thread separate from the UI thread and counts the current time every 500 milliseconds.
For the current time, the date (date
) and time (time
) are disclosed to the outside using ReadOnlyObjectProperty
, respectively.
ClockBase.java
protected final ReadOnlyObjectWrapper<LocalDate> date = new ReadOnlyObjectWrapper<>();
protected final ReadOnlyObjectWrapper<LocalTime> time = new ReadOnlyObjectWrapper<>();
...
@Override
public ReadOnlyObjectProperty<LocalDate> dateProperty() {
return this.date.getReadOnlyProperty();
}
@Override
public ReadOnlyObjectProperty<LocalTime> timeProperty() {
return this.time.getReadOnlyProperty();
}
ReadOnlyObjectWrapper
is a class that can be used to expose read-only properties to the outside world.
You can use this to prevent the value from being rewritten from outside the model.
One thing to note is that the value is updated in a thread separate from the UI thread, so you must always use Platform.runLater ()
to update the property value from the UI thread. ..
If you update the value using runLater ()
in the model, the view side can use the property with confidence.
view
TimeLabelViewModel.java
package littlesky.view.main;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
...
import littlesky.model.clock.Clock;
import littlesky.model.option.ViewOptions;
...
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import static littlesky.util.BindingBuilder.*;
public class TimeLabelViewModel {
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss");
private static final DateTimeFormatter formatterWithoutSeconds = DateTimeFormatter.ofPattern("HH:mm");
private final ReadOnlyStringWrapper text = new ReadOnlyStringWrapper("00:00:00");
...
public void bind(Clock clock, SkyColor skyColor, ViewOptions viewOptions) {
this.text.bind(
binding(clock.timeProperty(), viewOptions.showSecondsProperty())
.computeValue(() -> this.formatClockTime(clock, viewOptions))
);
...
}
private String formatClockTime(Clock clock, ViewOptions viewOptions) {
LocalTime time = clock.getTime();
return time.format(viewOptions.isShowSeconds() ? formatter : formatterWithoutSeconds);
}
...
public ReadOnlyStringProperty textProperty() {
return this.text.getReadOnlyProperty();
}
...
}
TimeLabelViewModel
is a class that controls the time display.
In the bind ()
method, we define the value of the text (text
property) to be displayed on the time label depending on Clock
and ViewOptions
.
public void bind(Clock clock, SkyColor skyColor, ViewOptions viewOptions) {
this.text.bind(
binding(clock.timeProperty(), viewOptions.showSecondsProperty())
.computeValue(() -> this.formatClockTime(clock, viewOptions))
);
...
}
--binding (...). ComputeValue (...)
is a builder that allows you to write Binding
instances with less description, and all you are doing is creating an anonymous class for ʻObjectBinding. is. --Implementation is [here](https://github.com/opengl-8080/little-sky/blob/master/src/main/java/littlesky/util/BindingBuilder.java) --Here it is defined that the time text (
text property) depends on the
timeproperty of
Clock and the
showSecondsproperty of
ViewOptions. --And if either the value of
time or
showSecondschanges, the
formatClockTime ()method is executed and the value of the
text` property is updated.
private String formatClockTime(Clock clock, ViewOptions viewOptions) {
LocalTime time = clock.getTime();
return time.format(viewOptions.isShowSeconds() ? formatter : formatterWithoutSeconds);
}
--formatClockTime ()
refers to the updated time
, showSeconds
to generate the time text to display on the screen.
--Finally, the text
property is exposed to the outside world as a read-only property, just like the model.
private final ReadOnlyStringWrapper text = new ReadOnlyStringWrapper("00:00:00");
...
public ReadOnlyStringProperty textProperty() {
return this.text.getReadOnlyProperty();
}
--The exposed text
property is associated with the item on the screen in the controller.
controller
MainController.java
package littlesky.controller.main;
...
public class MainController implements Initializable {
...
@FXML
private Label timeLabel;
...
@Override
public void initialize(URL url, ResourceBundle resources) {
...
this.replaceClockAndWeather(this.realTimeClock, this.openWeatherMap);
}
...
private void replaceClockAndWeather(Clock newClock, Weather weather) {
...
TimeLabelViewModel timeLabelViewModel = new TimeLabelViewModel();
timeLabelViewModel.bind(newClock, skyColor, this.options.getViewOptions());
this.timeLabel.textProperty().bind(timeLabelViewModel.textProperty());
...
}
...
}
--MainController
is injected with timeLabel
, which displays the time on the screen.
--The text
property of this timeLabel
and the text
property published by the previous TimeLabelViewModel
are linked by bind ()
.
RealTimeClock
TimeLabelViewModel
detects it and updates the displayed text, taking into account the display settings at that time.text
property of timeLabel
, which was linked with the text
property of TimeLavelViewModel
, is updated and the screen display changes.
-The model update is reflected in the view, such asThis is not the only correct answer, as it is a method that we arrived at by trial and error while implementing it. However, I feel that this method of using the JavaFX property monitoring mechanism as a way to propagate model changes to the view is not wrong in the direction.
** What I thought was good **
――I feel that the roles of each class are clearly separated. --Model updates main logic and values --View decides what to display based on model values --Control is a bridge between view and model ――I feel that the cooperation of these classes is kept simple by the JavaFX property monitoring mechanism. --The amount of code in the controller class has been reduced. --In JavaFX, inject a node object on the screen into the controller class --Therefore, it is easy to write the processing about the view in the controller class. ――However, as the number of items increased, the implementation of views increased, and I had the experience that the controller class quickly became bloated. ――Thanks to creating a view class this time and letting the controller bridge it, we were able to remove the processing about the view from the controller.
** Points I would like to improve **
--The model depends on the JavaFX API --Looking at the model layer from the DDD domain model idea, it seems that JavaFX's dependence on the API is a bit strange. --Separation from the infrastructure layer ――This time, all the roles of the infrastructure layer are treated as logic and plunged into the "model", but I feel that it is better to separate the ideals. ――But I wanted something that moves quickly, so I put it together in a "model" for the time being. ――I would like to think if it can be divided a little more neatly --Controller class mess ――Even though I was able to cut out the view processing, I feel that the processing such as initialization of the view class is messed up. ――I feel like I want to make it a little more refreshing (I have no idea)
Regarding the calculation of sunrise and sunset times, I searched for a library in Java and found the following two.
The former calculates the times of sunrise and sunset, and the latter also calculates the position of the sun, the position of the moon, and the phase of the moon.
The former seems to have been unmaintained for several years, and the latter library seems to be better considering this use.
However, when I actually used it, the sunrise and sunset times were both calculated with reasonable accuracy (a few minutes of error), but the calculation of the moon phase by the latter library was considerably different from the actual one. It was.
So, in the end, I decided to use the former library to calculate the sunrise and sunset.
SunriseSunsetTime
package littlesky.model.sun;
import com.luckycatlabs.sunrisesunset.SunriseSunsetCalculator;
import com.luckycatlabs.sunrisesunset.dto.Location;
import littlesky.model.location.UserLocation;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;
public class SunriseSunsetTime {
private final LocalDate localDate;
private final SunriseSunsetCalculator calculator;
private final TimeZone timeZone = TimeZone.getDefault();
public SunriseSunsetTime(UserLocation userLocation, LocalDate localDate) {
this.localDate = localDate;
Location location = new Location(userLocation.getLatitude(), userLocation.getLongitude());
this.calculator = new SunriseSunsetCalculator(location, this.timeZone);
}
public LocalTime sunriseTime() {
Calendar today = this.toCalendar(this.localDate);
Calendar sunriseTime = this.calculator.getOfficialSunriseCalendarForDate(today);
return this.toLocalTime(sunriseTime);
}
public LocalTime sunsetTime() {
Calendar today = this.toCalendar(this.localDate);
Calendar sunsetTime = this.calculator.getOfficialSunsetCalendarForDate(today);
return this.toLocalTime(sunsetTime);
}
private Calendar toCalendar(LocalDate localDate) {
Date date = Date.from(localDate.atStartOfDay(this.timeZone.toZoneId()).toInstant());
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
return calendar;
}
private LocalTime toLocalTime(Calendar calendar) {
return calendar.getTime().toInstant().atZone(this.timeZone.toZoneId()).toLocalTime();
}
}
The date object that the library wants is of type Calendar
, which may not have been maintained for several years.
Since it is Java 8 here, it was necessary to convert it to and use it with the Date & Time API.
Then, what happened to the calculation of the moon phase? I couldn't find the library, so I decided to calculate it myself.
However, it is not possible to calculate the orbit, so
-Easy way to find the age of the moon (abbreviation of the age of the moon) (No.0250) -Age --Wikipedia -Metonic cycle-Wikipedia
With reference to this area, I wrote a program to find the age of the moon with reasonable accuracy.
Specifically, starting from the age of January 1, 1999 (13.17), the number of days until the current date is calculated, and the age of the moon progressing during that period is calculated in consideration of the Metonic cycle.
The implementation is in MoonPhase.
Recommended Posts