diff --git a/.gitignore b/.gitignore index 2be9ec5..408bc31 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ bin/ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* *.dat + +#Can contain copyrighted music! +Music/ diff --git a/Announcements/7-day-dis.wav b/Announcements/7-day-dis.wav new file mode 100644 index 0000000..5524217 Binary files /dev/null and b/Announcements/7-day-dis.wav differ diff --git a/Announcements/message-dis.wav b/Announcements/message-dis.wav new file mode 100644 index 0000000..251cb1b Binary files /dev/null and b/Announcements/message-dis.wav differ diff --git a/Announcements/message-old.wav b/Announcements/message-old.wav new file mode 100644 index 0000000..586bf09 Binary files /dev/null and b/Announcements/message-old.wav differ diff --git a/jlayer2.jar b/jlayer2.jar new file mode 100644 index 0000000..d1b027d Binary files /dev/null and b/jlayer2.jar differ diff --git a/src/com/flaremicro/audio/AgnosticAudioSystem.java b/src/com/flaremicro/audio/AgnosticAudioSystem.java new file mode 100644 index 0000000..4b76aeb --- /dev/null +++ b/src/com/flaremicro/audio/AgnosticAudioSystem.java @@ -0,0 +1,286 @@ +package com.flaremicro.audio; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Random; + +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.DataLine; +import javax.sound.sampled.FloatControl; +import javax.sound.sampled.SourceDataLine; + +import javazoom.jl.decoder.JavaLayerException; +import javazoom.jl.player.Player; + +import com.flaremicro.util.Util; + +public class AgnosticAudioSystem implements GameAudioSystem { + + private Collection listeners = new ArrayList(); + private HashMap threads = new HashMap(); + private Random random = new Random(); + + public void init() { + + } + + public void destroy() { + + } + + public void addAudioFinishedListener(AudioFinishedListener listener) { + this.listeners.add(listener); + } + + public void removeAudioFinishedListener(AudioFinishedListener listener) { + this.listeners.remove(listener); + } + + public void removeAudioFinishedListeners() { + this.listeners.clear(); + } + + public String play(URL res) { + if (res.getPath().contains(".")) + { + String key = String.valueOf(random.nextLong()); + String ext = res.getPath().substring(res.getPath().lastIndexOf(".") + 1).toLowerCase(); + AudioSystemThread audioSystemThread = null; + if (ext.equalsIgnoreCase("mp3")) + audioSystemThread = new AgnosticMp3AudioSystemThread(res, key, this, false); + if (ext.equalsIgnoreCase("wav")) + audioSystemThread = new AgnosticWavAudioSystemThread(res, key, this, false); + + if (audioSystemThread != null) + { + synchronized (threads) + { + threads.put(key, audioSystemThread); + } + new Thread(audioSystemThread).start(); + return key; + } + } + return null; + } + + public void playbg(URL res) { + stopbg(); + if (res.getPath().contains(".")) + { + String ext = res.getPath().substring(res.getPath().lastIndexOf(".") + 1).toLowerCase(); + AudioSystemThread audioSystemThread = null; + if (ext.equalsIgnoreCase("mp3")) + audioSystemThread = new AgnosticMp3AudioSystemThread(res, "bg", this, true); + if (ext.equalsIgnoreCase("wav")) + audioSystemThread = new AgnosticWavAudioSystemThread(res, "bg", this, true); + + if (audioSystemThread != null) + { + synchronized (threads) + { + threads.put("bg", audioSystemThread); + } + new Thread(audioSystemThread).start(); + } + } + } + + public void stopbg() { + if (threads.containsKey("bg")) + threads.get("bg").stopMusic(); + stopped("bg", threads.get("bg")); + } + + public void stopped(String threadName, AudioSystemThread audioSystemThread) { + synchronized (threads) + { + if (audioSystemThread != null && audioSystemThread == threads.get(threadName)) + { + threads.remove(threadName); + } + } + for (AudioFinishedListener listener : listeners) + { + listener.audioFinished(threadName, audioSystemThread.resource); + } + } + + public void setVolume(float percent, String key) { + AudioSystemThread ast = threads.get(key); + if (ast != null) + { + ast.setVolume(percent); + } + } + + public void stop(String currentKey) { + AudioSystemThread ast = threads.get(currentKey); + if (ast != null) + { + ast.stopMusic(); + } + } +} + +abstract class AudioSystemThread implements Runnable { + protected final boolean looping; + protected final AgnosticAudioSystem system; + protected final URL resource; + protected final String threadName; + + public AudioSystemThread(URL resource, String threadName, AgnosticAudioSystem system, boolean looping) { + this.resource = resource; + this.looping = looping; + this.threadName = threadName; + this.system = system; + } + + public abstract void setVolume(float percent); + + public abstract void stopMusic(); + + public final void onStop() { + this.system.stopped(threadName, this); + } + +} + +class AgnosticMp3AudioSystemThread extends AudioSystemThread { + private boolean running = true; + private Player player = null; + private static final int FRAME_BUFFER = 2; + private float volume = 1F; + + public AgnosticMp3AudioSystemThread(URL resource, String threadName, AgnosticAudioSystem system, boolean looping) { + super(resource, threadName, system, looping); + } + + public void run() { + do + { + try + { + player = new Player(new BufferedInputStream(resource.openStream())); + player.setVolume(volume); + while (running && player.play(FRAME_BUFFER)); + } + catch (JavaLayerException e) + { + e.printStackTrace(); + } + catch (IOException e) + { + e.printStackTrace(); + } + } + while (running && looping); + super.onStop(); + } + + public void stopMusic() { + this.running = false; + if (player != null) + player.close(); + } + + @Override + public void setVolume(float percent) { + volume = percent; + if (player != null) + { + player.setVolume(volume); + } + } + +} + +class AgnosticWavAudioSystemThread extends AudioSystemThread { + private boolean running = true; + private SourceDataLine sourceLine = null; + private float volume = 1.0F; + + public AgnosticWavAudioSystemThread(URL resource, String threadName, AgnosticAudioSystem system, boolean looping) { + super(resource, threadName, system, looping); + } + + public void run() { + AudioInputStream audioStream = null; + do + { + try + { + audioStream = AudioSystem.getAudioInputStream(resource); + AudioFormat audioFormat = audioStream.getFormat(); + DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat); + + sourceLine = (SourceDataLine) AudioSystem.getLine(info); + sourceLine.open(); + sourceLine.start(); + + FloatControl volume = (FloatControl) sourceLine.getControl(FloatControl.Type.MASTER_GAIN); + volume.setValue(this.volume); + + int nBytesRead = 0; + byte[] abData = new byte[4096]; + while (nBytesRead != -1 && running) + { + try + { + nBytesRead = audioStream.read(abData, 0, abData.length); + } + catch (IOException e) + { + e.printStackTrace(); + } + if (nBytesRead >= 0) + { + @SuppressWarnings("unused") + int nBytesWritten = sourceLine.write(abData, 0, nBytesRead); + } + } + + } + catch (Exception e) + { + e.printStackTrace(); + } + finally + { + Util.cleanClose(audioStream); + if (sourceLine != null) + { + sourceLine.drain(); + sourceLine.close(); + } + } + } + while (running && looping); + onStop(); + } + + public void stopMusic() { + this.running = false; + if (sourceLine != null) + { + sourceLine.drain(); + sourceLine.close(); + } + } + + @Override + public void setVolume(float percent) { + if (sourceLine != null) + { + this.volume = percent; + FloatControl volume = (FloatControl) sourceLine.getControl(FloatControl.Type.MASTER_GAIN); + volume.setValue(percent); + } + } +} \ No newline at end of file diff --git a/src/com/flaremicro/audio/AudioFinishedListener.java b/src/com/flaremicro/audio/AudioFinishedListener.java new file mode 100644 index 0000000..07d9e87 --- /dev/null +++ b/src/com/flaremicro/audio/AudioFinishedListener.java @@ -0,0 +1,7 @@ +package com.flaremicro.audio; + +import java.net.URL; + +public interface AudioFinishedListener { + public void audioFinished(String key, URL resource); +} diff --git a/src/com/flaremicro/audio/AudioPlaylist.java b/src/com/flaremicro/audio/AudioPlaylist.java new file mode 100644 index 0000000..07ba0b6 --- /dev/null +++ b/src/com/flaremicro/audio/AudioPlaylist.java @@ -0,0 +1,83 @@ +package com.flaremicro.audio; + +import java.net.URL; +import java.util.Random; + +public class AudioPlaylist implements AudioFinishedListener{ + + + private final AgnosticAudioSystem aas; + + public AudioPlaylist(AgnosticAudioSystem aas) + { + this.aas = aas; + } + + String currentKey = null; + int currentPlaylistIndex = 0; + URL[] playlist = null; + + float currVol = 1F; + private boolean stopped = true; + + public void setPlaylist(URL[] url) + { + this.playlist = url; + this.currentPlaylistIndex = 0; + } + + public void shuffle(){ + Random rand = new Random(); + + for (int i = 0; i < playlist.length; i++) { + int randomIndexToSwap = rand.nextInt(playlist.length); + URL temp = playlist[randomIndexToSwap]; + playlist[randomIndexToSwap] = playlist[i]; + playlist[i] = temp; + } + } + + public void start() + { + stopped = false; + currentPlaylistIndex = 0; + aas.addAudioFinishedListener(this); + if(currentKey == null && playlist != null && playlist.length > 0) + { + currentKey = aas.play(playlist[0]); + aas.setVolume(currVol, currentKey); + } + } + + public void next() + { + if(playlist != null && playlist.length > 0) + { + currentPlaylistIndex = (currentPlaylistIndex + 1 ) % playlist.length; + currentKey = aas.play(playlist[currentPlaylistIndex]); + aas.setVolume(currVol, currentKey); + } + } + + public void audioFinished(String key, URL resource) { + if(!stopped && key == currentKey) + { + next(); + } + } + + public void setVolume(float volume) { + currVol = volume; + aas.setVolume(currVol, currentKey); + } + + public void stop() { + stopped = true; + aas.stop(currentKey); + } + + public float getVolume() { + return currVol; + } + +} diff --git a/src/com/flaremicro/audio/GameAudioSystem.java b/src/com/flaremicro/audio/GameAudioSystem.java new file mode 100644 index 0000000..c14707b --- /dev/null +++ b/src/com/flaremicro/audio/GameAudioSystem.java @@ -0,0 +1,16 @@ +package com.flaremicro.audio; + +import java.net.URL; + +public interface GameAudioSystem { + + public void init(); + + public void destroy(); + + public void playbg(URL res); + + public String play(URL res); + + public void stopbg(); +} diff --git a/src/com/flaremicro/visualforecast/AudioManager.java b/src/com/flaremicro/visualforecast/AudioManager.java new file mode 100644 index 0000000..b1da640 --- /dev/null +++ b/src/com/flaremicro/visualforecast/AudioManager.java @@ -0,0 +1,130 @@ +package com.flaremicro.visualforecast; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashSet; +import com.flaremicro.audio.AgnosticAudioSystem; +import com.flaremicro.audio.AudioFinishedListener; +import com.flaremicro.audio.AudioPlaylist; + +public class AudioManager implements AudioFinishedListener { + + private PropertyManager propertyManager; + private AgnosticAudioSystem aas; + private AudioPlaylist audioPlaylist; + + private File playlistDirectory = new File("./Music"); + + HashSet playingKeys = new HashSet(); + + public AudioManager(PropertyManager propertyManager) { + this.propertyManager = propertyManager; + this.aas = new AgnosticAudioSystem(); + this.aas.addAudioFinishedListener(this); + this.audioPlaylist = new AudioPlaylist(aas); + updatePlaylist(); + } + + private void updatePlaylist() { + playlistDirectory = propertyManager.getFile("music-dir", playlistDirectory); + if (playlistDirectory == null || !playlistDirectory.isDirectory()) + return; + File[] files = playlistDirectory.listFiles(); + URL[] urls = new URL[files.length]; + for (int i = 0; i < files.length; i++) + { + try + { + urls[i] = files[i].toURI().toURL(); + } + catch (MalformedURLException e) + { + e.printStackTrace(); + } + } + audioPlaylist.setPlaylist(urls); + audioPlaylist.shuffle(); + } + + @Override + public void audioFinished(String key, URL resource) { + playingKeys.remove(key); + if (playingKeys.size() == 0) + rampVolume(1F); + } + + public void startPlaylist() { + if (playlistDirectory == null || !playlistDirectory.isDirectory()) + return; + audioPlaylist.start(); + } + + public void stopPlaylist() { + audioPlaylist.stop(); + } + + public void end() { + audioPlaylist.stop(); + aas.destroy(); + } + + public void playAnnouncement(File file) { + try + { + URL url = file.toURI().toURL(); + rampVolume(-10F); + playingKeys.add(aas.play(url)); + } + catch (MalformedURLException e) + { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + Thread currThread = null; + private float volume = 1F; + + //***horrible*** + private void rampVolume(float newVolume) { + this.volume = newVolume; + if (currThread != null) + return; + currThread = new Thread(new Runnable() { + + @Override + public void run() { + + try + { + if (volume < audioPlaylist.getVolume()) + { + while (audioPlaylist.getVolume() > volume) + { + audioPlaylist.setVolume(audioPlaylist.getVolume() - 0.1F); + Thread.sleep(10L); + } + } + else if (volume > audioPlaylist.getVolume()) + { + while (audioPlaylist.getVolume() < volume) + { + audioPlaylist.setVolume(audioPlaylist.getVolume() + 0.1F); + Thread.sleep(10L); + } + } + } + catch (InterruptedException ex) + { + + } + audioPlaylist.setVolume(volume); + currThread = null; + } + + }); + currThread.start(); + } + +} diff --git a/src/com/flaremicro/visualforecast/displays/impl/ThirtySixHourForecastDisplay.java b/src/com/flaremicro/visualforecast/displays/impl/ThirtySixHourForecastDisplay.java new file mode 100644 index 0000000..5e9dd8b --- /dev/null +++ b/src/com/flaremicro/visualforecast/displays/impl/ThirtySixHourForecastDisplay.java @@ -0,0 +1,232 @@ +package com.flaremicro.visualforecast.displays.impl; + +import java.awt.Color; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.image.BufferedImage; +import java.util.ArrayList; + +import com.flaremicro.visualforecast.RenderPanel; +import com.flaremicro.visualforecast.api.ForecastProvider; +import com.flaremicro.visualforecast.displays.Display; +import com.flaremicro.visualforecast.forecast.ForecastDetails; +import com.flaremicro.visualforecast.forecast.TownForecast; +import com.flaremicro.visualforecast.graphics.DrawingUtil; +import com.flaremicro.visualforecast.graphics.FontManager; + +import static com.flaremicro.visualforecast.graphics.RenderConstants.*; + +public class ThirtySixHourForecastDisplay implements Display { + + private int townIndex = 0; + private TownForecast[] townForecasts; + private String[] lines = new String[0]; + private Font font; + + private int stringOffset = 0; + private int ticksBeforeChange = 0; + + public ThirtySixHourForecastDisplay() { + font = FontManager.getInstance().getOrCreateFont(Font.TRUETYPE_FONT, this.getClass().getResource("/Star4000.ttf")).deriveFont(30F); + + /* + int w2 = g2d.getFontMetrics().stringWidth("EXTREME WEATHER ADVISORY"); + drawOutlinedString(g2d, (W >> 1) - (w2 >> 1), TOPBAR_HEIGHT + 48, "EXTREME WEATHER ADVISORY", Color.RED, Color.BLACK, 2); + + g2d.setFont(font.deriveFont(30F)); + for (int i = 0; i < testString.length; i++) + { + drawOutlinedString(g2d, 90, TOPBAR_HEIGHT + 78 + 25 * i, testString[i], Color.WHITE, Color.BLACK, 1); + } + */ + } + + @Override + public void tick(RenderPanel renderer, long ticks, int iconTicks) { + ticksBeforeChange--; + if (ticksBeforeChange <= 0) + { + stringOffset += 9; + if (stringOffset < lines.length) + { + ticksBeforeChange = 200; + renderer.requestExclusiveBoundedRepaint(); + } + else + { + if(!setNextTown(renderer)) + { + renderer.nextDisplay(); + } + else + { + ticksBeforeChange = 200; + renderer.requestExclusiveBoundedRepaint(); + } + } + } + } + + @Override + public void initDisplay(RenderPanel renderer, ForecastProvider forecastProvider, long ticks, int iconTicks) { + ForecastDetails details = forecastProvider != null ? forecastProvider.getForecast() : null; + renderer.setCurrentForecast("36 Hour Detailed Forecast"); + townIndex = -1; + if(details == null || details.getTownForecast() == null || details.getTownForecast().length <= 0) + { + renderer.nextDisplay(); + return; + } + else + { + townForecasts = details.getTownForecast(); + if(!setNextTown(renderer)) + { + renderer.nextDisplay(); + return; + } + } + } + + public boolean setNextTown(RenderPanel renderer) + { + townIndex++; + if(townIndex >= townForecasts.length) + return false; + + TownForecast town = townForecasts[townIndex]; + + if(!town.isDisplaySupported(getDisplayName()) || town.getDetailedForecast() == null || town.getDetailedForecast().length <= 0) + { + return setNextTown(renderer); + } + ticksBeforeChange = 200; + stringOffset = 0; + + StringBuilder forecastBuilder = new StringBuilder(); + for(int i = 0; i < town.getDetailedForecast().length; i++) + { + if(i > 0) + forecastBuilder.append("\n\n"); + forecastBuilder.append(town.getDetailedForecast()[i].title + ":\n"); + forecastBuilder.append(town.getDetailedForecast()[i].status); + } + + ArrayList linesList = new ArrayList(); + + renderer.setCurrentTown(town.getTownName()); + + BufferedImage disposableImage = new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY); + Graphics2D g = disposableImage.createGraphics(); + FontMetrics f = g.getFontMetrics(font); + String[] lines = forecastBuilder.toString().split("\n"); + for (int i = 0; i < lines.length; i++) + { + splitText(lines[i], W - 150 - STROKE_WIDTH, f, linesList); + //if(words[i]) + } + + g.dispose(); + disposableImage.flush(); + + redrawRegionlost(renderer); + + this.lines = linesList.toArray(new String[0]); + return true; + } + + private void splitText(String text, int maxWidth, FontMetrics fontMetrics, ArrayList lineList) { + + text = text.trim(); + //this.text.add(text); + if (fontMetrics.stringWidth(text) <= maxWidth) + lineList.add(text); + else + { + while (text.length() > 0) + { + int idx = binSearch(text, fontMetrics, maxWidth, 1, text.length()); + if (idx <= 0) + break; + String texPart = text.substring(0, idx).trim(); + if (fontMetrics.stringWidth(text.substring(0, Math.min(text.length(), idx + 1))) > maxWidth && texPart.contains(" ")) + { + idx = texPart.lastIndexOf(" "); + texPart = texPart.substring(0, idx); + } + lineList.add(texPart); + text = text.substring(idx).trim(); + } + } + } + + private static int binSearch(String string, FontMetrics fontMetrics, int key, int low, int high) { + int index = 0; + + while (low <= high) + { + int mid = low + ((high - low) / 2); + int midmetric = fontMetrics.stringWidth(string.substring(0, mid)); + if (midmetric < key) + { + low = mid + 1; + index = mid; + } + else if (midmetric > key) + { + high = mid - 1; + } + else if (midmetric == key) + { + index = mid; + break; + } + } + return index; + } + + @Override + public void drawDisplay(RenderPanel renderer, Graphics2D g2d, long ticks, int iconTicks) { + } + + @Override + public void drawBoundLimitedDisplay(RenderPanel renderer, Graphics2D g2d, Rectangle bounds, long ticks, int iconTicks) { + DrawingUtil.drawGradientRect(g2d, 60, TOPBAR_HEIGHT, W - 120, MAINBAR_HEIGHT, 20, BG_BLUE.darker(), BG_BLUE.brighter()); + g2d.setFont(font); + FontMetrics fontMetrics = g2d.getFontMetrics(); + g2d.setColor(BG_BLUE.darker()); + g2d.drawRect(60 + STROKE_OFFSET, TOPBAR_HEIGHT + STROKE_OFFSET, W - 120 - STROKE_WIDTH, MAINBAR_HEIGHT - STROKE_WIDTH); + for (int i = 0; i < Math.min(9, lines.length - this.stringOffset); i++) + { + if (lines[i + stringOffset].startsWith("*") && lines[i].endsWith("*")) + { + String line = lines[i + stringOffset].substring(1, lines[i + stringOffset].length() - 1); + DrawingUtil.drawOutlinedString(g2d, 60 + 20 + STROKE_OFFSET, TOPBAR_HEIGHT + STROKE_OFFSET + 40 + i * 30, "*", Color.WHITE, Color.BLACK, 2); + DrawingUtil.drawOutlinedString(g2d, 60 + 20 + STROKE_OFFSET + (W - 150 - STROKE_WIDTH) / 2 - fontMetrics.stringWidth(line) / 2, TOPBAR_HEIGHT + STROKE_OFFSET + 40 + i * 30, line, Color.WHITE, Color.BLACK, 2); + DrawingUtil.drawOutlinedString(g2d, W - 95 - STROKE_WIDTH, TOPBAR_HEIGHT + STROKE_OFFSET + 40 + i * 30, "*", Color.WHITE, Color.BLACK, 2); + } + else if (lines[i + stringOffset].startsWith("[") && lines[i].endsWith("]")) + { + String line = lines[i + stringOffset].substring(1, lines[i + stringOffset].length() - 1); + DrawingUtil.drawOutlinedString(g2d, 60 + 20 + STROKE_OFFSET + (W - 150 - STROKE_WIDTH) / 2 - fontMetrics.stringWidth(line) / 2, TOPBAR_HEIGHT + STROKE_OFFSET + 40 + i * 30, line, Color.WHITE, Color.BLACK, 2); + } + else DrawingUtil.drawOutlinedString(g2d, 60 + 20 + STROKE_OFFSET, TOPBAR_HEIGHT + STROKE_OFFSET + 40 + i * 30, lines[i + stringOffset], Color.WHITE, Color.BLACK, 2); + } + } + + @Override + public void redrawRegionlost(RenderPanel renderer) { + renderer.addRedrawBound(TOPBAR_HEIGHT, W - 120, MAINBAR_HEIGHT, 20); + } + + @Override + public void notifyForecastProviderUpdate(RenderPanel renderer, ForecastProvider forecastProvider) { + } + + @Override + public String getDisplayName() { + return "36-hour"; + } +} diff --git a/src/com/flaremicro/visualforecast/forecast/DetailedForecast.java b/src/com/flaremicro/visualforecast/forecast/DetailedForecast.java new file mode 100644 index 0000000..e82fa2f --- /dev/null +++ b/src/com/flaremicro/visualforecast/forecast/DetailedForecast.java @@ -0,0 +1,12 @@ +package com.flaremicro.visualforecast.forecast; + +public class DetailedForecast { + public final String title; + public final String status; + + public DetailedForecast(String title, String status) + { + this.title = title; + this.status = status; + } +}