Refactor Map-Storage to be more modular in preparation for SQL-storage

This commit is contained in:
Blue (Lukas Rieger) 2021-11-06 16:14:14 +01:00
parent 3608c050ac
commit 49e956ed66
No known key found for this signature in database
GPG Key ID: 904C4995F9E1F800
19 changed files with 606 additions and 241 deletions

View File

@ -35,6 +35,8 @@
import de.bluecolored.bluemap.core.mca.MCAWorld; import de.bluecolored.bluemap.core.mca.MCAWorld;
import de.bluecolored.bluemap.core.resourcepack.ParseResourceException; import de.bluecolored.bluemap.core.resourcepack.ParseResourceException;
import de.bluecolored.bluemap.core.resourcepack.ResourcePack; import de.bluecolored.bluemap.core.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.storage.FileStorage;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.world.World; import de.bluecolored.bluemap.core.world.World;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
@ -164,11 +166,16 @@ private synchronized void loadWorldsAndMaps() throws IOException, InterruptedExc
} }
} }
Storage storage = new FileStorage(
getRenderConfig().getWebRoot().toPath().resolve("data"),
mapConfig.getCompression()
);
BmMap map = new BmMap( BmMap map = new BmMap(
id, id,
name, name,
world, world,
getRenderConfig().getWebRoot().toPath().resolve("data").resolve(id), storage,
getResourcePack(), getResourcePack(),
mapConfig mapConfig
); );

View File

@ -35,7 +35,6 @@
import de.bluecolored.bluemap.common.rendermanager.MapUpdateTask; import de.bluecolored.bluemap.common.rendermanager.MapUpdateTask;
import de.bluecolored.bluemap.common.rendermanager.RenderManager; import de.bluecolored.bluemap.common.rendermanager.RenderManager;
import de.bluecolored.bluemap.common.rendermanager.WorldRegionRenderTask; import de.bluecolored.bluemap.common.rendermanager.WorldRegionRenderTask;
import de.bluecolored.bluemap.core.map.BmMap;
import de.bluecolored.bluemap.core.world.Grid; import de.bluecolored.bluemap.core.world.Grid;
import java.io.IOException; import java.io.IOException;
@ -96,7 +95,7 @@ public boolean scheduleMapUpdateTask(BlueMapMap map, Collection<Vector2i> region
@Override @Override
public boolean scheduleMapPurgeTask(BlueMapMap map) throws IOException { public boolean scheduleMapPurgeTask(BlueMapMap map) throws IOException {
BlueMapMapImpl cmap = castMap(map); BlueMapMapImpl cmap = castMap(map);
return renderManager.scheduleRenderTask(new MapPurgeTask(cmap.getMapType())); return renderManager.scheduleRenderTask(MapPurgeTask.create(cmap.getMapType()));
} }
@Override @Override

View File

@ -64,7 +64,6 @@
import de.bluecolored.bluemap.core.world.World; import de.bluecolored.bluemap.core.world.World;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.*; import java.util.*;
import java.util.function.Function; import java.util.function.Function;
@ -810,41 +809,33 @@ public int purgeCommand(CommandContext<S> context) {
CommandSource source = commandSourceInterface.apply(context.getSource()); CommandSource source = commandSourceInterface.apply(context.getSource());
// parse map argument // parse map argument
String mapId = context.getArgument("map", String.class); String mapString = context.getArgument("map", String.class);
BmMap map = parseMap(mapString).orElse(null);
if (map == null) {
source.sendMessage(Text.of(TextColor.RED, "There is no ", helper.mapHelperHover(), " with this name: ", TextColor.WHITE, mapString));
return 0;
}
new Thread(() -> { new Thread(() -> {
try { try {
Path mapFolder = plugin.getRenderConfig().getWebRoot().toPath().resolve("data").resolve(mapId);
if (!Files.isDirectory(mapFolder)) {
source.sendMessage(Text.of(TextColor.RED, "There is no map-data to purge for the map-id '" + mapId + "'!"));
return;
}
Optional<BmMap> optMap = parseMap(mapId);
// delete map // delete map
MapPurgeTask purgeTask; MapPurgeTask purgeTask = MapPurgeTask.create(map);
if (optMap.isPresent()){
purgeTask = new MapPurgeTask(optMap.get());
} else {
purgeTask = new MapPurgeTask(mapFolder);
}
plugin.getRenderManager().scheduleRenderTaskNext(purgeTask); plugin.getRenderManager().scheduleRenderTaskNext(purgeTask);
source.sendMessage(Text.of(TextColor.GREEN, "Created new Task to purge map '" + mapId + "'")); source.sendMessage(Text.of(TextColor.GREEN, "Created new Task to purge map '" + map.getId() + "'"));
// if map is loaded, reset it and start updating it after the purge // reset the map and start updating it after the purge
if (optMap.isPresent()) { RenderTask updateTask = new MapUpdateTask(map);
RenderTask updateTask = new MapUpdateTask(optMap.get());
plugin.getRenderManager().scheduleRenderTask(updateTask); plugin.getRenderManager().scheduleRenderTask(updateTask);
source.sendMessage(Text.of(TextColor.GREEN, "Created new Update-Task for map '" + mapId + "'")); source.sendMessage(Text.of(TextColor.GREEN, "Created new Update-Task for map '" + map.getId() + "'"));
source.sendMessage(Text.of(TextColor.GRAY, "If you don't want to render this map again, you need to remove it from your configuration first!")); source.sendMessage(Text.of(TextColor.GRAY, "If you don't this map to render again after the purge, use ",
} TextColor.DARK_GRAY, "/bluemap freeze " + map.getId(), TextColor.GRAY, " first!"));
source.sendMessage(Text.of(TextColor.GREEN, "Use ", TextColor.GRAY, "/bluemap", TextColor.GREEN, " to see the progress.")); source.sendMessage(Text.of(TextColor.GREEN, "Use ", TextColor.GRAY, "/bluemap", TextColor.GREEN, " to see the progress."));
} catch (IOException | IllegalArgumentException e) { } catch (IOException | IllegalArgumentException e) {
source.sendMessage(Text.of(TextColor.RED, "There was an error trying to purge '" + mapId + "', see console for details.")); source.sendMessage(Text.of(TextColor.RED, "There was an error trying to purge '" + map.getId() + "', see console for details."));
Logger.global.logError("Failed to purge map '" + mapId + "'!", e); Logger.global.logError("Failed to purge map '" + map.getId() + "'!", e);
} }
}).start(); }).start();

View File

@ -26,33 +26,52 @@
import de.bluecolored.bluemap.core.debug.DebugDump; import de.bluecolored.bluemap.core.debug.DebugDump;
import de.bluecolored.bluemap.core.map.BmMap; import de.bluecolored.bluemap.core.map.BmMap;
import de.bluecolored.bluemap.core.storage.FileStorage;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.util.FileUtils; import de.bluecolored.bluemap.core.util.FileUtils;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.Objects;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class MapPurgeTask implements RenderTask { public abstract class MapPurgeTask implements RenderTask {
@DebugDump private final BmMap map; public static MapPurgeTask create(BmMap map) throws IOException {
@DebugDump private final Path directory; Storage storage = map.getStorage();
@DebugDump private final int subFilesCount; if (storage instanceof FileStorage) {
return new MapFilePurgeTask(map, (FileStorage) storage);
} else {
return new MapStoragePurgeTask(map);
}
}
public static MapPurgeTask create(Path mapDirectory) throws IOException {
return new MapFilePurgeTask(mapDirectory);
}
@DebugDump
private static class MapFilePurgeTask extends MapPurgeTask {
private final BmMap map;
private final Path directory;
private final int subFilesCount;
private final LinkedList<Path> subFiles; private final LinkedList<Path> subFiles;
@DebugDump private volatile boolean hasMoreWork; private volatile boolean hasMoreWork;
@DebugDump private volatile boolean cancelled; private volatile boolean cancelled;
public MapPurgeTask(Path mapDirectory) throws IOException { public MapFilePurgeTask(Path mapDirectory) throws IOException {
this(null, mapDirectory); this(null, mapDirectory);
} }
public MapPurgeTask(BmMap map) throws IOException { public MapFilePurgeTask(BmMap map, FileStorage fileStorage) throws IOException {
this(map, map.getFileRoot()); this(map, fileStorage.getFilePath(map.getId()));
} }
private MapPurgeTask(BmMap map, Path directory) throws IOException { private MapFilePurgeTask(BmMap map, Path directory) throws IOException {
this.map = map; this.map = map;
this.directory = directory; this.directory = directory;
this.subFiles = Files.walk(directory, 3) this.subFiles = Files.walk(directory, 3)
@ -94,6 +113,7 @@ public boolean hasMoreWork() {
} }
@Override @Override
@DebugDump
public double estimateProgress() { public double estimateProgress() {
return 1d - (subFiles.size() / (double) subFilesCount); return 1d - (subFiles.size() / (double) subFilesCount);
} }
@ -106,8 +126,9 @@ public void cancel() {
@Override @Override
public boolean contains(RenderTask task) { public boolean contains(RenderTask task) {
if (task == this) return true; if (task == this) return true;
if (task instanceof MapPurgeTask) { if (task instanceof MapFilePurgeTask) {
return ((MapPurgeTask) task).directory.toAbsolutePath().normalize().startsWith(this.directory.toAbsolutePath().normalize()); return ((MapFilePurgeTask) task).directory.toAbsolutePath().normalize()
.startsWith(this.directory.toAbsolutePath().normalize());
} }
return false; return false;
@ -119,3 +140,65 @@ public String getDescription() {
} }
} }
@DebugDump
private static class MapStoragePurgeTask extends MapPurgeTask {
private final BmMap map;
private volatile boolean hasMoreWork;
public MapStoragePurgeTask(BmMap map) {
this.map = Objects.requireNonNull(map);
this.hasMoreWork = true;
}
@Override
public void doWork() throws Exception {
synchronized (this) {
if (!this.hasMoreWork) return;
this.hasMoreWork = false;
}
try {
map.getStorage().purgeMap(map.getId());
} finally {
// reset map render state
map.getRenderState().reset();
}
}
@Override
public boolean hasMoreWork() {
return this.hasMoreWork;
}
@Override
@DebugDump
public double estimateProgress() {
return 0d;
}
@Override
public void cancel() {
this.hasMoreWork = false;
}
@Override
public boolean contains(RenderTask task) {
if (task == this) return true;
if (task instanceof MapStoragePurgeTask) {
return map.equals(((MapStoragePurgeTask) task).map);
}
return false;
}
@Override
public String getDescription() {
return "Purge Map " + map.getId();
}
}
}

View File

@ -28,6 +28,7 @@
import com.flowpowered.math.vector.Vector3i; import com.flowpowered.math.vector.Vector3i;
import de.bluecolored.bluemap.core.debug.DebugDump; import de.bluecolored.bluemap.core.debug.DebugDump;
import de.bluecolored.bluemap.core.map.MapSettings; import de.bluecolored.bluemap.core.map.MapSettings;
import de.bluecolored.bluemap.core.storage.Compression;
import de.bluecolored.bluemap.core.util.ConfigUtils; import de.bluecolored.bluemap.core.util.ConfigUtils;
import org.spongepowered.configurate.ConfigurationNode; import org.spongepowered.configurate.ConfigurationNode;
@ -53,7 +54,7 @@ public class MapConfig implements MapSettings {
private Vector3i min, max; private Vector3i min, max;
private boolean renderEdges; private boolean renderEdges;
private boolean useGzip; private Compression compression;
private boolean ignoreMissingLightData; private boolean ignoreMissingLightData;
private int hiresTileSize; private int hiresTileSize;
@ -106,7 +107,7 @@ public MapConfig(ConfigurationNode node) throws IOException {
this.renderEdges = node.node("renderEdges").getBoolean(true); this.renderEdges = node.node("renderEdges").getBoolean(true);
//useCompression //useCompression
this.useGzip = node.node("useCompression").getBoolean(true); this.compression = node.node("useCompression").getBoolean(true) ? Compression.GZIP : Compression.NONE;
//ignoreMissingLightData //ignoreMissingLightData
this.ignoreMissingLightData = node.node("ignoreMissingLightData").getBoolean(false); this.ignoreMissingLightData = node.node("ignoreMissingLightData").getBoolean(false);
@ -142,6 +143,10 @@ public int getSkyColor() {
return skyColor; return skyColor;
} }
public Compression getCompression() {
return compression;
}
@Override @Override
public float getAmbientLight() { public float getAmbientLight() {
return ambientLight; return ambientLight;
@ -196,9 +201,4 @@ public boolean isRenderEdges() {
return renderEdges; return renderEdges;
} }
@Override
public boolean useGzipCompression() {
return useGzip;
}
} }

View File

@ -35,10 +35,10 @@
@DebugDump @DebugDump
public class RenderConfig { public class RenderConfig {
private File webRoot = new File("web"); private File webRoot;
private boolean useCookies; private boolean useCookies;
private boolean enableFreeFlight; private boolean enableFreeFlight;
private List<MapConfig> mapConfigs = new ArrayList<>(); private List<MapConfig> mapConfigs;
public RenderConfig(ConfigurationNode node) throws IOException { public RenderConfig(ConfigurationNode node) throws IOException {

View File

@ -28,16 +28,19 @@
import de.bluecolored.bluemap.core.debug.DebugDump; import de.bluecolored.bluemap.core.debug.DebugDump;
import de.bluecolored.bluemap.core.logger.Logger; import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.map.hires.HiresModelManager; import de.bluecolored.bluemap.core.map.hires.HiresModelManager;
import de.bluecolored.bluemap.core.map.lowres.LowresModelManager;
import de.bluecolored.bluemap.core.map.hires.HiresTileMeta; import de.bluecolored.bluemap.core.map.hires.HiresTileMeta;
import de.bluecolored.bluemap.core.map.lowres.LowresModelManager;
import de.bluecolored.bluemap.core.resourcepack.ResourcePack; import de.bluecolored.bluemap.core.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.storage.*;
import de.bluecolored.bluemap.core.world.Grid; import de.bluecolored.bluemap.core.world.Grid;
import de.bluecolored.bluemap.core.world.World; import de.bluecolored.bluemap.core.world.World;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate; import java.util.function.Predicate;
@DebugDump @DebugDump
@ -46,7 +49,7 @@ public class BmMap {
private final String id; private final String id;
private final String name; private final String name;
private final World world; private final World world;
private final Path fileRoot; private final Storage storage;
private final MapRenderState renderState; private final MapRenderState renderState;
@ -58,38 +61,37 @@ public class BmMap {
private long renderTimeSumNanos; private long renderTimeSumNanos;
private long tilesRendered; private long tilesRendered;
public BmMap(String id, String name, World world, Path fileRoot, ResourcePack resourcePack, MapSettings settings) throws IOException { public BmMap(String id, String name, World world, Storage storage, ResourcePack resourcePack, MapSettings settings) throws IOException {
this.id = Objects.requireNonNull(id); this.id = Objects.requireNonNull(id);
this.name = Objects.requireNonNull(name); this.name = Objects.requireNonNull(name);
this.world = Objects.requireNonNull(world); this.world = Objects.requireNonNull(world);
this.fileRoot = Objects.requireNonNull(fileRoot); this.storage = Objects.requireNonNull(storage);
Objects.requireNonNull(resourcePack); Objects.requireNonNull(resourcePack);
Objects.requireNonNull(settings); Objects.requireNonNull(settings);
this.renderState = new MapRenderState(); this.renderState = new MapRenderState();
File rstateFile = getRenderStateFile(); Optional<InputStream> rstateData = storage.readMeta(id, MetaType.RENDER_STATE);
if (rstateFile.exists()) { if (rstateData.isPresent()) {
try { try (InputStream in = rstateData.get()){
this.renderState.load(rstateFile); this.renderState.load(in);
} catch (IOException ex) { } catch (IOException ex) {
Logger.global.logWarning("Failed to load render-state for map '" + getId() + "': " + ex); Logger.global.logWarning("Failed to load render-state for map '" + getId() + "': " + ex);
} }
} }
this.hiresModelManager = new HiresModelManager( this.hiresModelManager = new HiresModelManager(
fileRoot.resolve("hires"), storage.tileStorage(id, TileType.HIRES),
resourcePack, resourcePack,
settings, settings,
new Grid(settings.getHiresTileSize(), 2) new Grid(settings.getHiresTileSize(), 2)
); );
this.lowresModelManager = new LowresModelManager( this.lowresModelManager = new LowresModelManager(
fileRoot.resolve("lowres"), storage.tileStorage(id, TileType.LOWRES),
new Vector2i(settings.getLowresPointsPerLowresTile(), settings.getLowresPointsPerLowresTile()), new Vector2i(settings.getLowresPointsPerLowresTile(), settings.getLowresPointsPerLowresTile()),
new Vector2i(settings.getLowresPointsPerHiresTile(), settings.getLowresPointsPerHiresTile()), new Vector2i(settings.getLowresPointsPerHiresTile(), settings.getLowresPointsPerHiresTile())
settings.useGzipCompression()
); );
this.tileFilter = t -> true; this.tileFilter = t -> true;
@ -116,17 +118,13 @@ public void renderTile(Vector2i tile) {
public synchronized void save() { public synchronized void save() {
lowresModelManager.save(); lowresModelManager.save();
try { try (OutputStream out = storage.writeMeta(id, MetaType.RENDER_STATE)) {
this.renderState.save(getRenderStateFile()); this.renderState.save(out);
} catch (IOException ex){ } catch (IOException ex){
Logger.global.logError("Failed to save render-state for map: '" + this.id + "'!", ex); Logger.global.logError("Failed to save render-state for map: '" + this.id + "'!", ex);
} }
} }
public File getRenderStateFile() {
return fileRoot.resolve(".rstate").toFile();
}
public String getId() { public String getId() {
return id; return id;
} }
@ -139,8 +137,8 @@ public World getWorld() {
return world; return world;
} }
public Path getFileRoot() { public Storage getStorage() {
return fileRoot; return storage;
} }
public MapRenderState getRenderState() { public MapRenderState getRenderState() {
@ -189,7 +187,7 @@ public String toString() {
"id='" + id + '\'' + "id='" + id + '\'' +
", name='" + name + '\'' + ", name='" + name + '\'' +
", world=" + world + ", world=" + world +
", fileRoot=" + fileRoot + ", storage=" + storage +
'}'; '}';
} }

View File

@ -26,7 +26,6 @@
import com.flowpowered.math.vector.Vector2i; import com.flowpowered.math.vector.Vector2i;
import de.bluecolored.bluemap.core.debug.DebugDump; import de.bluecolored.bluemap.core.debug.DebugDump;
import de.bluecolored.bluemap.core.util.AtomicFileHelper;
import java.io.*; import java.io.*;
import java.util.HashMap; import java.util.HashMap;
@ -57,12 +56,9 @@ public synchronized void reset() {
regionRenderTimes.clear(); regionRenderTimes.clear();
} }
public synchronized void save(File file) throws IOException { public synchronized void save(OutputStream out) throws IOException {
OutputStream fOut = AtomicFileHelper.createFilepartOutputStream(file);
GZIPOutputStream gOut = new GZIPOutputStream(fOut);
try ( try (
DataOutputStream dOut = new DataOutputStream(gOut) DataOutputStream dOut = new DataOutputStream(new GZIPOutputStream(out))
) { ) {
dOut.writeInt(regionRenderTimes.size()); dOut.writeInt(regionRenderTimes.size());
@ -79,13 +75,11 @@ public synchronized void save(File file) throws IOException {
} }
} }
public synchronized void load(File file) throws IOException { public synchronized void load(InputStream in) throws IOException {
regionRenderTimes.clear(); regionRenderTimes.clear();
try ( try (
FileInputStream fIn = new FileInputStream(file); DataInputStream dIn = new DataInputStream(new GZIPInputStream(in))
GZIPInputStream gIn = new GZIPInputStream(fIn);
DataInputStream dIn = new DataInputStream(gIn)
) { ) {
int size = dIn.readInt(); int size = dIn.readInt();
@ -98,7 +92,7 @@ public synchronized void load(File file) throws IOException {
regionRenderTimes.put(regionPos, renderTime); regionRenderTimes.put(regionPos, renderTime);
} }
} } catch (EOFException ignore){} // ignoring a sudden end of stream, since it is save to only read as many as we can
} }
} }

View File

@ -28,34 +28,27 @@
import com.flowpowered.math.vector.Vector3i; import com.flowpowered.math.vector.Vector3i;
import de.bluecolored.bluemap.core.logger.Logger; import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.resourcepack.ResourcePack; import de.bluecolored.bluemap.core.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.util.AtomicFileHelper; import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.util.FileUtils;
import de.bluecolored.bluemap.core.world.Grid; import de.bluecolored.bluemap.core.world.Grid;
import de.bluecolored.bluemap.core.world.World; import de.bluecolored.bluemap.core.world.World;
import java.io.*; import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.zip.GZIPOutputStream;
public class HiresModelManager { public class HiresModelManager {
private final Path fileRoot; private final Storage.TileStorage storage;
private final HiresModelRenderer renderer; private final HiresModelRenderer renderer;
private final Grid tileGrid; private final Grid tileGrid;
private final boolean useGzip;
public HiresModelManager(Path fileRoot, ResourcePack resourcePack, RenderSettings renderSettings, Grid tileGrid) { public HiresModelManager(Storage.TileStorage storage, ResourcePack resourcePack, RenderSettings renderSettings, Grid tileGrid) {
this(fileRoot, new HiresModelRenderer(resourcePack, renderSettings), tileGrid, renderSettings.useGzipCompression()); this(storage, new HiresModelRenderer(resourcePack, renderSettings), tileGrid);
} }
public HiresModelManager(Path fileRoot, HiresModelRenderer renderer, Grid tileGrid, boolean useGzip) { public HiresModelManager(Storage.TileStorage storage, HiresModelRenderer renderer, Grid tileGrid) {
this.fileRoot = fileRoot; this.storage = storage;
this.renderer = renderer; this.renderer = renderer;
this.tileGrid = tileGrid; this.tileGrid = tileGrid;
this.useGzip = useGzip;
} }
/** /**
@ -79,25 +72,10 @@ public HiresTileMeta render(World world, Vector2i tile) {
} }
private void save(final HiresTileModel model, Vector2i tile) { private void save(final HiresTileModel model, Vector2i tile) {
File file = getFile(tile, useGzip); try (OutputStream os = storage.write(tile)) {
OutputStream os = null;
try {
os = AtomicFileHelper.createFilepartOutputStream(file);
os = new BufferedOutputStream(os);
if (useGzip) os = new GZIPOutputStream(os);
model.writeBufferGeometryJson(os); model.writeBufferGeometryJson(os);
} catch (IOException e){ } catch (IOException e){
Logger.global.logError("Failed to save hires model: " + file, e); Logger.global.logError("Failed to save hires model: " + tile, e);
} finally {
try {
if (os != null) {
os.close();
}
} catch (IOException e) {
Logger.global.logError("Failed to close file: " + file, e);
}
} }
} }
@ -108,11 +86,4 @@ public Grid getTileGrid() {
return tileGrid; return tileGrid;
} }
/**
* Returns the file for a tile
*/
public File getFile(Vector2i tilePos, boolean gzip){
return FileUtils.coordsToFile(fileRoot, tilePos, "json" + (gzip ? ".gz" : ""));
}
} }

View File

@ -73,13 +73,6 @@ default boolean isRenderEdges() {
return true; return true;
} }
/**
* If gzip compression will be used to compress the generated files
*/
default boolean useGzipCompression() {
return true;
}
default boolean isInsideRenderBoundaries(int x, int z) { default boolean isInsideRenderBoundaries(int x, int z) {
Vector3i min = getMin(); Vector3i min = getMin();
Vector3i max = getMax(); Vector3i max = getMax();

View File

@ -26,16 +26,17 @@
import com.flowpowered.math.vector.Vector2i; import com.flowpowered.math.vector.Vector2i;
import com.flowpowered.math.vector.Vector3f; import com.flowpowered.math.vector.Vector3f;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.threejs.BufferGeometry; import de.bluecolored.bluemap.core.threejs.BufferGeometry;
import de.bluecolored.bluemap.core.util.AtomicFileHelper;
import de.bluecolored.bluemap.core.util.MathUtils; import de.bluecolored.bluemap.core.util.MathUtils;
import de.bluecolored.bluemap.core.util.ModelUtils; import de.bluecolored.bluemap.core.util.ModelUtils;
import java.io.*; import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.zip.GZIPOutputStream;
public class LowresModel { public class LowresModel {
@ -79,7 +80,7 @@ public void update(Vector2i point, float height, Vector3f color){
* Saves this model to its file * Saves this model to its file
* @param force if this is false, the model is only saved if it has any changes * @param force if this is false, the model is only saved if it has any changes
*/ */
public void save(File file, boolean force, boolean useGzip) throws IOException { public void save(Storage.TileStorage storage, Vector2i tile, boolean force) throws IOException {
if (!force && !hasUnsavedChanges) return; if (!force && !hasUnsavedChanges) return;
this.hasUnsavedChanges = false; this.hasUnsavedChanges = false;
@ -91,11 +92,9 @@ public void save(File file, boolean force, boolean useGzip) throws IOException {
} }
synchronized (fileLock) { synchronized (fileLock) {
OutputStream os = new BufferedOutputStream(AtomicFileHelper.createFilepartOutputStream(file));
if (useGzip) os = new GZIPOutputStream(os);
OutputStreamWriter osw = new OutputStreamWriter(os, StandardCharsets.UTF_8);
try ( try (
PrintWriter pw = new PrintWriter(osw) PrintWriter pw = new PrintWriter(
new OutputStreamWriter(storage.write(tile), StandardCharsets.UTF_8))
){ ){
pw.print(json); pw.print(json);
} }

View File

@ -28,42 +28,36 @@
import com.flowpowered.math.vector.Vector3f; import com.flowpowered.math.vector.Vector3f;
import de.bluecolored.bluemap.core.logger.Logger; import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.map.hires.HiresTileMeta; import de.bluecolored.bluemap.core.map.hires.HiresTileMeta;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.threejs.BufferGeometry; import de.bluecolored.bluemap.core.threejs.BufferGeometry;
import de.bluecolored.bluemap.core.util.FileUtils;
import de.bluecolored.bluemap.core.util.math.Color; import de.bluecolored.bluemap.core.util.math.Color;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.zip.GZIPInputStream;
public class LowresModelManager { public class LowresModelManager {
private final Path fileRoot; private final Storage.TileStorage storage;
private final Vector2i pointsPerLowresTile; private final Vector2i pointsPerLowresTile;
private final Vector2i pointsPerHiresTile; private final Vector2i pointsPerHiresTile;
private final boolean useGzip;
private final Map<File, CachedModel> models; private final Map<Vector2i, CachedModel> models;
public LowresModelManager(Path fileRoot, Vector2i pointsPerLowresTile, Vector2i pointsPerHiresTile, boolean useGzip) { public LowresModelManager(Storage.TileStorage storage, Vector2i pointsPerLowresTile, Vector2i pointsPerHiresTile) {
this.fileRoot = fileRoot; this.storage = storage;
this.pointsPerLowresTile = pointsPerLowresTile; this.pointsPerLowresTile = pointsPerLowresTile;
this.pointsPerHiresTile = pointsPerHiresTile; this.pointsPerHiresTile = pointsPerHiresTile;
models = new ConcurrentHashMap<>(); models = new ConcurrentHashMap<>();
this.useGzip = useGzip;
} }
/** /**
@ -116,7 +110,7 @@ public void render(HiresTileMeta tileMeta) {
* Saves all unsaved changes to the models to disk * Saves all unsaved changes to the models to disk
*/ */
public synchronized void save(){ public synchronized void save(){
for (Entry<File, CachedModel> entry : models.entrySet()){ for (Entry<Vector2i, CachedModel> entry : models.entrySet()){
saveModel(entry.getKey(), entry.getValue()); saveModel(entry.getKey(), entry.getValue());
} }
@ -159,39 +153,31 @@ public void update(int px, int pz, float height, Color color) {
} }
} }
/**
* Returns the file for a tile
*/
public File getFile(Vector2i tile, boolean useGzip){
return FileUtils.coordsToFile(fileRoot, tile, "json" + (useGzip ? ".gz" : ""));
}
private LowresModel getModel(Vector2i tile) { private LowresModel getModel(Vector2i tile) {
File modelFile = getFile(tile, useGzip); CachedModel model = models.get(tile);
CachedModel model = models.get(modelFile);
if (model == null){ if (model == null){
synchronized (this) { synchronized (this) {
model = models.get(modelFile); model = models.get(tile);
if (model == null){ if (model == null){
if (modelFile.exists()){ try {
try (FileInputStream fis = new FileInputStream(modelFile)) { Optional<InputStream> optIs = storage.read(tile);
InputStream is = fis; if (optIs.isPresent()){
if (useGzip) is = new GZIPInputStream(is); try (InputStream is = optIs.get()) {
String json = IOUtils.toString(is, StandardCharsets.UTF_8); String json = IOUtils.toString(is, StandardCharsets.UTF_8);
model = new CachedModel(BufferGeometry.fromJson(json)); model = new CachedModel(BufferGeometry.fromJson(json));
}
}
} catch (IllegalArgumentException | IOException ex){ } catch (IllegalArgumentException | IOException ex){
Logger.global.logWarning("Failed to load lowres model '" + modelFile + "': " + ex); Logger.global.logWarning("Failed to load lowres model '" + tile + "': " + ex);
try { try {
FileUtils.delete(modelFile); storage.delete(tile);
} catch (IOException ex2) { } catch (IOException ex2) {
Logger.global.logError("Failed to delete lowres-file: " + modelFile, ex2); Logger.global.logError("Failed to delete lowres-file: " + tile, ex2);
}
} }
} }
@ -199,7 +185,7 @@ private LowresModel getModel(Vector2i tile) {
model = new CachedModel(pointsPerLowresTile); model = new CachedModel(pointsPerLowresTile);
} }
models.put(modelFile, model); models.put(tile, model);
tidyUpModelCache(); tidyUpModelCache();
} }
@ -217,12 +203,12 @@ private LowresModel getModel(Vector2i tile) {
* This method gets automatically called if the cache grows, but if you want to ensure model will be saved after 2 minutes, you could e.g call this method every second.<br> * This method gets automatically called if the cache grows, but if you want to ensure model will be saved after 2 minutes, you could e.g call this method every second.<br>
*/ */
public synchronized void tidyUpModelCache() { public synchronized void tidyUpModelCache() {
List<Entry<File, CachedModel>> entries = new ArrayList<>(models.size()); List<Entry<Vector2i, CachedModel>> entries = new ArrayList<>(models.size());
entries.addAll(models.entrySet()); entries.addAll(models.entrySet());
entries.sort((e1, e2) -> (int) Math.signum(e1.getValue().cacheTime - e2.getValue().cacheTime)); entries.sort((e1, e2) -> (int) Math.signum(e1.getValue().cacheTime - e2.getValue().cacheTime));
int size = entries.size(); int size = entries.size();
for (Entry<File, CachedModel> e : entries) { for (Entry<Vector2i, CachedModel> e : entries) {
if (size > 10) { if (size > 10) {
saveAndRemoveModel(e.getKey(), e.getValue()); saveAndRemoveModel(e.getKey(), e.getValue());
continue; continue;
@ -234,22 +220,22 @@ public synchronized void tidyUpModelCache() {
} }
} }
private synchronized void saveAndRemoveModel(File modelFile, CachedModel model) { private synchronized void saveAndRemoveModel(Vector2i tile, CachedModel model) {
models.remove(modelFile); models.remove(tile);
try { try {
model.save(modelFile, false, useGzip); model.save(storage, tile,false);
//logger.logDebug("Saved and unloaded lowres tile: " + model.getTile()); //logger.logDebug("Saved and unloaded lowres tile: " + model.getTile());
} catch (IOException ex) { } catch (IOException ex) {
Logger.global.logError("Failed to save and unload lowres-model: " + modelFile, ex); Logger.global.logError("Failed to save and unload lowres-model: " + tile, ex);
} }
} }
private void saveModel(File modelFile, CachedModel model) { private void saveModel(Vector2i tile, CachedModel model) {
try { try {
model.save(modelFile, false, useGzip); model.save(storage, tile, false);
//logger.logDebug("Saved lowres tile: " + model.getTile()); //logger.logDebug("Saved lowres tile: " + model.getTile());
} catch (IOException ex) { } catch (IOException ex) {
Logger.global.logError("Failed to save lowres-model: " + modelFile, ex); Logger.global.logError("Failed to save lowres-model: " + tile, ex);
} }
model.resetCacheTime(); model.resetCacheTime();

View File

@ -0,0 +1,75 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.storage;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
public enum Compression {
NONE("") {
@Override
public OutputStream compress(OutputStream out) {
return out;
}
@Override
public InputStream decompress(InputStream in) {
return in;
}
},
GZIP(".gz"){
@Override
public OutputStream compress(OutputStream out) throws IOException {
return new GZIPOutputStream(out);
}
@Override
public InputStream decompress(InputStream in) throws IOException {
return new GZIPInputStream(in);
}
};
private final String fileSuffix;
Compression(String fileSuffix) {
this.fileSuffix = fileSuffix;
}
public String getFileSuffix() {
return fileSuffix;
}
public abstract OutputStream compress(OutputStream out) throws IOException;
public abstract InputStream decompress(InputStream in) throws IOException;
}

View File

@ -0,0 +1,143 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.storage;
import com.flowpowered.math.vector.Vector2i;
import de.bluecolored.bluemap.core.debug.DebugDump;
import de.bluecolored.bluemap.core.util.AtomicFileHelper;
import de.bluecolored.bluemap.core.util.FileUtils;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.*;
@DebugDump
public class FileStorage extends Storage {
private static final EnumMap<MetaType, String> metaTypeFileNames = new EnumMap<>(MetaType.class);
static {
metaTypeFileNames.put(MetaType.TEXTURES, "../textures.json");
metaTypeFileNames.put(MetaType.SETTINGS, "../settings.json");
metaTypeFileNames.put(MetaType.MARKERS, "../markers.json");
metaTypeFileNames.put(MetaType.RENDER_STATE, ".rstate");
}
private final Path root;
private final Compression compression;
public FileStorage(Path root, Compression compression) {
this.root = root;
this.compression = compression;
}
@Override
public OutputStream writeMapTile(String mapId, TileType tileType, Vector2i tile) throws IOException {
Path file = getFilePath(mapId, tileType, tile);
OutputStream os = AtomicFileHelper.createFilepartOutputStream(file);
os = new BufferedOutputStream(os);
os = compression.compress(os);
return os;
}
@Override
public Optional<InputStream> readMapTile(String mapId, TileType tileType, Vector2i tile) throws IOException {
Path file = getFilePath(mapId, tileType, tile);
if (!Files.exists(file)) return Optional.empty();
InputStream is = Files.newInputStream(file, StandardOpenOption.READ);
is = new BufferedInputStream(is);
return Optional.of(is);
}
@Override
public void deleteMapTile(String mapId, TileType tileType, Vector2i tile) throws IOException {
Path file = getFilePath(mapId, tileType, tile);
FileUtils.delete(file.toFile());
}
@Override
public OutputStream writeMeta(String mapId, MetaType metaType) throws IOException {
Path file = getFilePath(mapId).resolve(getFilename(metaType));
OutputStream os = AtomicFileHelper.createFilepartOutputStream(file);
os = new BufferedOutputStream(os);
return os;
}
@Override
public Optional<InputStream> readMeta(String mapId, MetaType metaType) throws IOException {
Path file = getFilePath(mapId).resolve(getFilename(metaType));
if (!Files.exists(file)) return Optional.empty();
InputStream is = Files.newInputStream(file, StandardOpenOption.READ);
is = new BufferedInputStream(is);
return Optional.of(is);
}
@Override
public void purgeMap(String mapId) throws IOException {
FileUtils.delete(getFilePath(mapId).toFile());
}
public Path getFilePath(String mapId, TileType tileType, Vector2i tile){
String path = "x" + tile.getX() + "z" + tile.getY();
char[] cs = path.toCharArray();
List<String> folders = new ArrayList<>();
StringBuilder folder = new StringBuilder();
for (char c : cs){
folder.append(c);
if (c >= '0' && c <= '9'){
folders.add(folder.toString());
folder.delete(0, folder.length());
}
}
String fileName = folders.remove(folders.size() - 1);
Path p = getFilePath(mapId).resolve(tileType.getTypeId());
for (String s : folders){
p = p.resolve(s);
}
return p.resolve(fileName + ".json" + compression.getFileSuffix());
}
public Path getFilePath(String mapId) {
return root.resolve(mapId);
}
private static String getFilename(MetaType metaType) {
return metaTypeFileNames.getOrDefault(metaType, metaType.name().toLowerCase(Locale.ROOT));
}
}

View File

@ -0,0 +1,10 @@
package de.bluecolored.bluemap.core.storage;
public enum MetaType {
TEXTURES,
SETTINGS,
MARKERS,
RENDER_STATE
}

View File

@ -0,0 +1,76 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.storage;
import com.flowpowered.math.vector.Vector2i;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Optional;
public abstract class Storage {
public abstract OutputStream writeMapTile(String mapId, TileType tileType, Vector2i tile) throws IOException;
public abstract Optional<InputStream> readMapTile(String mapId, TileType tileType, Vector2i tile) throws IOException;
public abstract void deleteMapTile(String mapId, TileType tileType, Vector2i tile) throws IOException;
public abstract OutputStream writeMeta(String mapId, MetaType metaType) throws IOException;
public abstract Optional<InputStream> readMeta(String mapId, MetaType metaType) throws IOException;
public abstract void purgeMap(String mapId) throws IOException;
public TileStorage tileStorage(final String mapId, final TileType tileType) {
return new TileStorage(mapId, tileType);
}
public class TileStorage {
private final String mapId;
private final TileType tileType;
private TileStorage(String mapId, TileType tileType) {
this.mapId = mapId;
this.tileType = tileType;
}
public OutputStream write(Vector2i tile) throws IOException {
return Storage.this.writeMapTile(mapId, tileType, tile);
}
public Optional<InputStream> read(Vector2i tile) throws IOException {
return Storage.this.readMapTile(mapId, tileType, tile);
}
public void delete(Vector2i tile) throws IOException {
deleteMapTile(mapId, tileType, tile);
}
}
}

View File

@ -0,0 +1,41 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.storage;
public enum TileType {
HIRES ("hires"),
LOWRES ("lowres");
private final String typeId;
TileType(String typeId) {
this.typeId = typeId;
}
public String getTypeId() {
return typeId;
}
}

View File

@ -31,8 +31,6 @@
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.regex.Pattern; import java.util.regex.Pattern;
public class FileUtils { public class FileUtils {

View File

@ -47,6 +47,7 @@
import org.spongepowered.api.config.ConfigDir; import org.spongepowered.api.config.ConfigDir;
import org.spongepowered.api.entity.living.player.server.ServerPlayer; import org.spongepowered.api.entity.living.player.server.ServerPlayer;
import org.spongepowered.api.event.Listener; import org.spongepowered.api.event.Listener;
import org.spongepowered.api.event.block.TickBlockEvent;
import org.spongepowered.api.event.lifecycle.RefreshGameEvent; import org.spongepowered.api.event.lifecycle.RefreshGameEvent;
import org.spongepowered.api.event.lifecycle.RegisterCommandEvent; import org.spongepowered.api.event.lifecycle.RegisterCommandEvent;
import org.spongepowered.api.event.lifecycle.StartedEngineEvent; import org.spongepowered.api.event.lifecycle.StartedEngineEvent;