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.resourcepack.ParseResourceException;
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 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(
id,
name,
world,
getRenderConfig().getWebRoot().toPath().resolve("data").resolve(id),
storage,
getResourcePack(),
mapConfig
);

View File

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

View File

@ -64,7 +64,6 @@
import de.bluecolored.bluemap.core.world.World;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Function;
@ -810,41 +809,33 @@ public int purgeCommand(CommandContext<S> context) {
CommandSource source = commandSourceInterface.apply(context.getSource());
// 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(() -> {
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
MapPurgeTask purgeTask;
if (optMap.isPresent()){
purgeTask = new MapPurgeTask(optMap.get());
} else {
purgeTask = new MapPurgeTask(mapFolder);
}
MapPurgeTask purgeTask = MapPurgeTask.create(map);
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
if (optMap.isPresent()) {
RenderTask updateTask = new MapUpdateTask(optMap.get());
plugin.getRenderManager().scheduleRenderTask(updateTask);
source.sendMessage(Text.of(TextColor.GREEN, "Created new Update-Task for map '" + mapId + "'"));
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!"));
}
// reset the map and start updating it after the purge
RenderTask updateTask = new MapUpdateTask(map);
plugin.getRenderManager().scheduleRenderTask(updateTask);
source.sendMessage(Text.of(TextColor.GREEN, "Created new Update-Task for map '" + map.getId() + "'"));
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."));
} catch (IOException | IllegalArgumentException e) {
source.sendMessage(Text.of(TextColor.RED, "There was an error trying to purge '" + mapId + "', see console for details."));
Logger.global.logError("Failed to purge map '" + mapId + "'!", e);
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 '" + map.getId() + "'!", e);
}
}).start();

View File

@ -26,96 +26,179 @@
import de.bluecolored.bluemap.core.debug.DebugDump;
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 java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedList;
import java.util.Objects;
import java.util.stream.Collectors;
public class MapPurgeTask implements RenderTask {
public abstract class MapPurgeTask implements RenderTask {
@DebugDump private final BmMap map;
@DebugDump private final Path directory;
@DebugDump private final int subFilesCount;
private final LinkedList<Path> subFiles;
@DebugDump private volatile boolean hasMoreWork;
@DebugDump private volatile boolean cancelled;
public MapPurgeTask(Path mapDirectory) throws IOException {
this(null, mapDirectory);
public static MapPurgeTask create(BmMap map) throws IOException {
Storage storage = map.getStorage();
if (storage instanceof FileStorage) {
return new MapFilePurgeTask(map, (FileStorage) storage);
} else {
return new MapStoragePurgeTask(map);
}
}
public MapPurgeTask(BmMap map) throws IOException {
this(map, map.getFileRoot());
public static MapPurgeTask create(Path mapDirectory) throws IOException {
return new MapFilePurgeTask(mapDirectory);
}
private MapPurgeTask(BmMap map, Path directory) throws IOException {
this.map = map;
this.directory = directory;
this.subFiles = Files.walk(directory, 3)
.collect(Collectors.toCollection(LinkedList::new));
this.subFilesCount = subFiles.size();
this.hasMoreWork = true;
this.cancelled = false;
@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 volatile boolean hasMoreWork;
private volatile boolean cancelled;
public MapFilePurgeTask(Path mapDirectory) throws IOException {
this(null, mapDirectory);
}
public MapFilePurgeTask(BmMap map, FileStorage fileStorage) throws IOException {
this(map, fileStorage.getFilePath(map.getId()));
}
private MapFilePurgeTask(BmMap map, Path directory) throws IOException {
this.map = map;
this.directory = directory;
this.subFiles = Files.walk(directory, 3)
.collect(Collectors.toCollection(LinkedList::new));
this.subFilesCount = subFiles.size();
this.hasMoreWork = true;
this.cancelled = false;
}
@Override
public void doWork() throws Exception {
synchronized (this) {
if (!this.hasMoreWork) return;
this.hasMoreWork = false;
}
try {
// delete subFiles first to be able to track the progress and cancel
while (!subFiles.isEmpty()) {
Path subFile = subFiles.getLast();
FileUtils.delete(subFile.toFile());
subFiles.removeLast();
if (this.cancelled) return;
}
// make sure everything is deleted
FileUtils.delete(directory.toFile());
} finally {
// reset map render state
if (this.map != null) {
this.map.getRenderState().reset();
}
}
}
@Override
public boolean hasMoreWork() {
return this.hasMoreWork;
}
@Override
@DebugDump
public double estimateProgress() {
return 1d - (subFiles.size() / (double) subFilesCount);
}
@Override
public void cancel() {
this.cancelled = true;
}
@Override
public boolean contains(RenderTask task) {
if (task == this) return true;
if (task instanceof MapFilePurgeTask) {
return ((MapFilePurgeTask) task).directory.toAbsolutePath().normalize()
.startsWith(this.directory.toAbsolutePath().normalize());
}
return false;
}
@Override
public String getDescription() {
return "Purge Map " + directory.getFileName();
}
}
@Override
public void doWork() throws Exception {
synchronized (this) {
if (!this.hasMoreWork) return;
@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;
}
try {
// delete subFiles first to be able to track the progress and cancel
while (!subFiles.isEmpty()) {
Path subFile = subFiles.getLast();
FileUtils.delete(subFile.toFile());
subFiles.removeLast();
if (this.cancelled) return;
@Override
public boolean contains(RenderTask task) {
if (task == this) return true;
if (task instanceof MapStoragePurgeTask) {
return map.equals(((MapStoragePurgeTask) task).map);
}
// make sure everything is deleted
FileUtils.delete(directory.toFile());
} finally {
// reset map render state
if (this.map != null) {
this.map.getRenderState().reset();
}
}
}
@Override
public boolean hasMoreWork() {
return this.hasMoreWork;
}
@Override
public double estimateProgress() {
return 1d - (subFiles.size() / (double) subFilesCount);
}
@Override
public void cancel() {
this.cancelled = true;
}
@Override
public boolean contains(RenderTask task) {
if (task == this) return true;
if (task instanceof MapPurgeTask) {
return ((MapPurgeTask) task).directory.toAbsolutePath().normalize().startsWith(this.directory.toAbsolutePath().normalize());
return false;
}
@Override
public String getDescription() {
return "Purge Map " + map.getId();
}
return false;
}
@Override
public String getDescription() {
return "Purge Map " + directory.getFileName();
}
}
}

View File

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

View File

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

View File

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

View File

@ -26,7 +26,6 @@
import com.flowpowered.math.vector.Vector2i;
import de.bluecolored.bluemap.core.debug.DebugDump;
import de.bluecolored.bluemap.core.util.AtomicFileHelper;
import java.io.*;
import java.util.HashMap;
@ -57,12 +56,9 @@ public synchronized void reset() {
regionRenderTimes.clear();
}
public synchronized void save(File file) throws IOException {
OutputStream fOut = AtomicFileHelper.createFilepartOutputStream(file);
GZIPOutputStream gOut = new GZIPOutputStream(fOut);
public synchronized void save(OutputStream out) throws IOException {
try (
DataOutputStream dOut = new DataOutputStream(gOut)
DataOutputStream dOut = new DataOutputStream(new GZIPOutputStream(out))
) {
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();
try (
FileInputStream fIn = new FileInputStream(file);
GZIPInputStream gIn = new GZIPInputStream(fIn);
DataInputStream dIn = new DataInputStream(gIn)
DataInputStream dIn = new DataInputStream(new GZIPInputStream(in))
) {
int size = dIn.readInt();
@ -98,7 +92,7 @@ public synchronized void load(File file) throws IOException {
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 de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.util.AtomicFileHelper;
import de.bluecolored.bluemap.core.util.FileUtils;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.world.Grid;
import de.bluecolored.bluemap.core.world.World;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.zip.GZIPOutputStream;
public class HiresModelManager {
private final Path fileRoot;
private final Storage.TileStorage storage;
private final HiresModelRenderer renderer;
private final Grid tileGrid;
private final boolean useGzip;
public HiresModelManager(Path fileRoot, ResourcePack resourcePack, RenderSettings renderSettings, Grid tileGrid) {
this(fileRoot, new HiresModelRenderer(resourcePack, renderSettings), tileGrid, renderSettings.useGzipCompression());
public HiresModelManager(Storage.TileStorage storage, ResourcePack resourcePack, RenderSettings renderSettings, Grid tileGrid) {
this(storage, new HiresModelRenderer(resourcePack, renderSettings), tileGrid);
}
public HiresModelManager(Path fileRoot, HiresModelRenderer renderer, Grid tileGrid, boolean useGzip) {
this.fileRoot = fileRoot;
public HiresModelManager(Storage.TileStorage storage, HiresModelRenderer renderer, Grid tileGrid) {
this.storage = storage;
this.renderer = renderer;
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) {
File file = getFile(tile, useGzip);
OutputStream os = null;
try {
os = AtomicFileHelper.createFilepartOutputStream(file);
os = new BufferedOutputStream(os);
if (useGzip) os = new GZIPOutputStream(os);
try (OutputStream os = storage.write(tile)) {
model.writeBufferGeometryJson(os);
} catch (IOException e){
Logger.global.logError("Failed to save hires model: " + file, e);
} finally {
try {
if (os != null) {
os.close();
}
} catch (IOException e) {
Logger.global.logError("Failed to close file: " + file, e);
}
Logger.global.logError("Failed to save hires model: " + tile, e);
}
}
@ -108,11 +86,4 @@ public Grid getTileGrid() {
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;
}
/**
* If gzip compression will be used to compress the generated files
*/
default boolean useGzipCompression() {
return true;
}
default boolean isInsideRenderBoundaries(int x, int z) {
Vector3i min = getMin();
Vector3i max = getMax();

View File

@ -26,16 +26,17 @@
import com.flowpowered.math.vector.Vector2i;
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.util.AtomicFileHelper;
import de.bluecolored.bluemap.core.util.MathUtils;
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.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.zip.GZIPOutputStream;
public class LowresModel {
@ -79,7 +80,7 @@ public void update(Vector2i point, float height, Vector3f color){
* Saves this model to its file
* @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;
this.hasUnsavedChanges = false;
@ -91,11 +92,9 @@ public void save(File file, boolean force, boolean useGzip) throws IOException {
}
synchronized (fileLock) {
OutputStream os = new BufferedOutputStream(AtomicFileHelper.createFilepartOutputStream(file));
if (useGzip) os = new GZIPOutputStream(os);
OutputStreamWriter osw = new OutputStreamWriter(os, StandardCharsets.UTF_8);
try (
PrintWriter pw = new PrintWriter(osw)
PrintWriter pw = new PrintWriter(
new OutputStreamWriter(storage.write(tile), StandardCharsets.UTF_8))
){
pw.print(json);
}

View File

@ -28,42 +28,36 @@
import com.flowpowered.math.vector.Vector3f;
import de.bluecolored.bluemap.core.logger.Logger;
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.util.FileUtils;
import de.bluecolored.bluemap.core.util.math.Color;
import org.apache.commons.io.IOUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.zip.GZIPInputStream;
public class LowresModelManager {
private final Path fileRoot;
private final Storage.TileStorage storage;
private final Vector2i pointsPerLowresTile;
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) {
this.fileRoot = fileRoot;
public LowresModelManager(Storage.TileStorage storage, Vector2i pointsPerLowresTile, Vector2i pointsPerHiresTile) {
this.storage = storage;
this.pointsPerLowresTile = pointsPerLowresTile;
this.pointsPerHiresTile = pointsPerHiresTile;
models = new ConcurrentHashMap<>();
this.useGzip = useGzip;
}
/**
@ -116,7 +110,7 @@ public void render(HiresTileMeta tileMeta) {
* Saves all unsaved changes to the models to disk
*/
public synchronized void save(){
for (Entry<File, CachedModel> entry : models.entrySet()){
for (Entry<Vector2i, CachedModel> entry : models.entrySet()){
saveModel(entry.getKey(), entry.getValue());
}
@ -159,47 +153,39 @@ 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) {
File modelFile = getFile(tile, useGzip);
CachedModel model = models.get(modelFile);
CachedModel model = models.get(tile);
if (model == null){
synchronized (this) {
model = models.get(modelFile);
model = models.get(tile);
if (model == null){
if (modelFile.exists()){
try (FileInputStream fis = new FileInputStream(modelFile)) {
InputStream is = fis;
if (useGzip) is = new GZIPInputStream(is);
try {
Optional<InputStream> optIs = storage.read(tile);
if (optIs.isPresent()){
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));
} catch (IllegalArgumentException | IOException ex){
Logger.global.logWarning("Failed to load lowres model '" + modelFile + "': " + ex);
try {
FileUtils.delete(modelFile);
} catch (IOException ex2) {
Logger.global.logError("Failed to delete lowres-file: " + modelFile, ex2);
model = new CachedModel(BufferGeometry.fromJson(json));
}
}
} catch (IllegalArgumentException | IOException ex){
Logger.global.logWarning("Failed to load lowres model '" + tile + "': " + ex);
try {
storage.delete(tile);
} catch (IOException ex2) {
Logger.global.logError("Failed to delete lowres-file: " + tile, ex2);
}
}
if (model == null){
model = new CachedModel(pointsPerLowresTile);
}
models.put(modelFile, model);
models.put(tile, model);
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>
*/
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.sort((e1, e2) -> (int) Math.signum(e1.getValue().cacheTime - e2.getValue().cacheTime));
int size = entries.size();
for (Entry<File, CachedModel> e : entries) {
for (Entry<Vector2i, CachedModel> e : entries) {
if (size > 10) {
saveAndRemoveModel(e.getKey(), e.getValue());
continue;
@ -234,22 +220,22 @@ public synchronized void tidyUpModelCache() {
}
}
private synchronized void saveAndRemoveModel(File modelFile, CachedModel model) {
models.remove(modelFile);
private synchronized void saveAndRemoveModel(Vector2i tile, CachedModel model) {
models.remove(tile);
try {
model.save(modelFile, false, useGzip);
model.save(storage, tile,false);
//logger.logDebug("Saved and unloaded lowres tile: " + model.getTile());
} 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 {
model.save(modelFile, false, useGzip);
model.save(storage, tile, false);
//logger.logDebug("Saved lowres tile: " + model.getTile());
} 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();

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.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.regex.Pattern;
public class FileUtils {

View File

@ -47,6 +47,7 @@
import org.spongepowered.api.config.ConfigDir;
import org.spongepowered.api.entity.living.player.server.ServerPlayer;
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.RegisterCommandEvent;
import org.spongepowered.api.event.lifecycle.StartedEngineEvent;