310 lines
8.9 KiB
Java
310 lines
8.9 KiB
Java
/*
|
|
* 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.render.lowres;
|
|
|
|
import java.io.File;
|
|
import java.io.FileInputStream;
|
|
import java.io.IOException;
|
|
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.UUID;
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
import java.util.zip.GZIPInputStream;
|
|
|
|
import org.apache.commons.io.IOUtils;
|
|
|
|
import com.flowpowered.math.vector.Vector2i;
|
|
import com.flowpowered.math.vector.Vector3d;
|
|
import com.flowpowered.math.vector.Vector3f;
|
|
import com.flowpowered.math.vector.Vector3i;
|
|
import com.flowpowered.math.vector.Vector4f;
|
|
|
|
import de.bluecolored.bluemap.core.logger.Logger;
|
|
import de.bluecolored.bluemap.core.render.hires.HiresModel;
|
|
import de.bluecolored.bluemap.core.threejs.BufferGeometry;
|
|
import de.bluecolored.bluemap.core.util.FileUtil;
|
|
|
|
public class LowresModelManager {
|
|
|
|
private Path fileRoot;
|
|
|
|
private Vector2i gridSize;
|
|
private Vector2i pointsPerHiresTile;
|
|
|
|
private Map<File, CachedModel> models;
|
|
|
|
public LowresModelManager(Path fileRoot, Vector2i gridSize, Vector2i pointsPerHiresTile) {
|
|
this.fileRoot = fileRoot;
|
|
|
|
this.gridSize = gridSize;
|
|
this.pointsPerHiresTile = pointsPerHiresTile;
|
|
|
|
models = new ConcurrentHashMap<>();
|
|
}
|
|
|
|
/**
|
|
* Renders all points from the given highres-model onto the lowres-grid
|
|
*/
|
|
public void render(HiresModel hiresModel) throws IOException {
|
|
Vector3i min = hiresModel.getBlockMin();
|
|
Vector3i max = hiresModel.getBlockMax();
|
|
Vector3i size = max.sub(min).add(Vector3i.ONE);
|
|
|
|
Vector2i blocksPerPoint =
|
|
size
|
|
.toVector2(true)
|
|
.div(pointsPerHiresTile);
|
|
|
|
Vector2i pointMin = min
|
|
.toVector2(true)
|
|
.toDouble()
|
|
.div(blocksPerPoint.toDouble())
|
|
.floor()
|
|
.toInt();
|
|
|
|
for (int tx = 0; tx < pointsPerHiresTile.getX(); tx++){
|
|
for (int tz = 0; tz < pointsPerHiresTile.getY(); tz++){
|
|
|
|
double height = 0;
|
|
|
|
Vector3d color = Vector3d.ZERO;
|
|
double colorCount = 0;
|
|
|
|
for (int x = 0; x < blocksPerPoint.getX(); x++){
|
|
for (int z = 0; z < blocksPerPoint.getY(); z++){
|
|
|
|
int rx = tx * blocksPerPoint.getX() + x + min.getX();
|
|
int rz = tz * blocksPerPoint.getY() + z + min.getZ();
|
|
height += hiresModel.getHeight(rx, rz);
|
|
|
|
Vector4f c = hiresModel.getColor(rx, rz);
|
|
color = color.add(c.toVector3().toDouble().mul(c.getW()));
|
|
colorCount += c.getW();
|
|
}
|
|
}
|
|
|
|
if (colorCount > 0) color = color.div(colorCount);
|
|
|
|
int count = blocksPerPoint.getX() * blocksPerPoint.getY();
|
|
height /= count;
|
|
|
|
Vector2i point = pointMin.add(tx, tz);
|
|
update(hiresModel.getWorld(), point, (float) height, color.toFloat());
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Saves all unsaved changes to the models to disk
|
|
*/
|
|
public synchronized void save(){
|
|
for (CachedModel model : models.values()){
|
|
saveModel(model);
|
|
}
|
|
|
|
tidyUpModelCache();
|
|
}
|
|
|
|
/**
|
|
* Updates a point on the lowresmodel-grid
|
|
*/
|
|
public void update(UUID world, Vector2i point, float height, Vector3f color) throws IOException {
|
|
Vector2i tile = pointToTile(point);
|
|
Vector2i relPoint = getPointRelativeToTile(tile, point);
|
|
LowresModel model = getModel(world, tile);
|
|
model.update(relPoint, height, color);
|
|
|
|
if (relPoint.getX() == 0){
|
|
Vector2i tile2 = tile.add(-1, 0);
|
|
Vector2i relPoint2 = getPointRelativeToTile(tile2, point);
|
|
LowresModel model2 = getModel(world, tile2);
|
|
model2.update(relPoint2, height, color);
|
|
}
|
|
|
|
if (relPoint.getY() == 0){
|
|
Vector2i tile2 = tile.add(0, -1);
|
|
Vector2i relPoint2 = getPointRelativeToTile(tile2, point);
|
|
LowresModel model2 = getModel(world, tile2);
|
|
model2.update(relPoint2, height, color);
|
|
}
|
|
|
|
if (relPoint.getX() == 0 && relPoint.getY() == 0){
|
|
Vector2i tile2 = tile.add(-1, -1);
|
|
Vector2i relPoint2 = getPointRelativeToTile(tile2, point);
|
|
LowresModel model2 = getModel(world, tile2);
|
|
model2.update(relPoint2, height, color);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the file for a tile
|
|
*/
|
|
public File getFile(Vector2i tile){
|
|
return FileUtil.coordsToFile(fileRoot, tile, "json.gz");
|
|
}
|
|
|
|
private LowresModel getModel(UUID world, Vector2i tile) throws IOException {
|
|
|
|
File modelFile = getFile(tile);
|
|
CachedModel model = models.get(modelFile);
|
|
|
|
if (model == null){
|
|
synchronized (this) {
|
|
model = models.get(modelFile);
|
|
if (model == null){
|
|
if (modelFile.exists()){
|
|
|
|
FileInputStream fis = new FileInputStream(modelFile);
|
|
try(
|
|
GZIPInputStream zis = new GZIPInputStream(fis);
|
|
){
|
|
String json = IOUtils.toString(zis, StandardCharsets.UTF_8);
|
|
|
|
try {
|
|
model = new CachedModel(world, tile, BufferGeometry.fromJson(json));
|
|
} catch (IllegalArgumentException | IOException ex){
|
|
Logger.global.logError("Failed to load lowres model: " + modelFile, ex);
|
|
//gridFile.renameTo(gridFile.toPath().getParent().resolve(gridFile.getName() + ".broken").toFile());
|
|
modelFile.delete();
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
if (model == null){
|
|
model = new CachedModel(world, tile, gridSize);
|
|
}
|
|
|
|
models.put(modelFile, model);
|
|
|
|
tidyUpModelCache();
|
|
}
|
|
}
|
|
}
|
|
|
|
return model;
|
|
}
|
|
|
|
/**
|
|
* This Method tidies up the model cache:<br>
|
|
* it saves all modified models that have not been saved for 2 minutes and<br>
|
|
* saves and removes the oldest models from the cache until the cache size is 10 or less.<br>
|
|
* <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() {
|
|
List<Entry<File, 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) {
|
|
if (size > 10) {
|
|
saveAndRemoveModel(e.getValue());
|
|
continue;
|
|
}
|
|
|
|
if (e.getValue().getCacheTime() > 120000) {
|
|
saveModel(e.getValue());
|
|
}
|
|
}
|
|
}
|
|
|
|
private synchronized void saveAndRemoveModel(CachedModel model) {
|
|
File modelFile = getFile(model.getTile());
|
|
models.remove(modelFile);
|
|
try {
|
|
model.save(modelFile, 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);
|
|
}
|
|
}
|
|
|
|
private void saveModel(CachedModel model) {
|
|
File modelFile = getFile(model.getTile());
|
|
try {
|
|
model.save(modelFile, false);
|
|
//logger.logDebug("Saved lowres tile: " + model.getTile());
|
|
} catch (IOException ex) {
|
|
Logger.global.logError("Failed to save lowres-model: " + modelFile, ex);
|
|
}
|
|
|
|
model.resetCacheTime();
|
|
}
|
|
|
|
private Vector2i pointToTile(Vector2i point){
|
|
return point
|
|
.toDouble()
|
|
.div(gridSize.toDouble())
|
|
.floor()
|
|
.toInt();
|
|
}
|
|
|
|
private Vector2i getPointRelativeToTile(Vector2i tile, Vector2i point){
|
|
return point.sub(tile.mul(gridSize));
|
|
}
|
|
|
|
public Vector2i getTileSize() {
|
|
return gridSize;
|
|
}
|
|
|
|
public Vector2i getPointsPerHiresTile() {
|
|
return pointsPerHiresTile;
|
|
}
|
|
|
|
private class CachedModel extends LowresModel {
|
|
|
|
private long cacheTime;
|
|
|
|
public CachedModel(UUID world, Vector2i tilePos, BufferGeometry model) {
|
|
super(world, tilePos, model);
|
|
|
|
cacheTime = System.currentTimeMillis();
|
|
}
|
|
|
|
public CachedModel(UUID world, Vector2i tilePos, Vector2i gridSize) {
|
|
super(world, tilePos, gridSize);
|
|
|
|
cacheTime = System.currentTimeMillis();
|
|
}
|
|
|
|
public long getCacheTime() {
|
|
return System.currentTimeMillis() - cacheTime;
|
|
}
|
|
|
|
public void resetCacheTime() {
|
|
cacheTime = System.currentTimeMillis();
|
|
}
|
|
|
|
}
|
|
|
|
}
|