@@ -20,19 +20,36 @@ public class TileManager {
2020 private final DiskTileCache diskCache ;
2121 private final ConcurrentHashMap <String , CompletableFuture <byte []>> pendingRequests ;
2222 private final ConcurrentHashMap <String , CachedChunkIndexes > chunkIndexCache ;
23+ private CompositeTileGenerator compositeTileGenerator ;
2324
2425 public TileManager (EasyWebMap plugin ) {
2526 this .plugin = plugin ;
2627 this .memoryCache = new TileCache (plugin .getConfig ().getTileCacheSize ());
2728 this .diskCache = new DiskTileCache (plugin .getDataDirectory ());
2829 this .pendingRequests = new ConcurrentHashMap <>();
2930 this .chunkIndexCache = new ConcurrentHashMap <>();
31+ // Initialize composite generator after this object is constructed
32+ this .compositeTileGenerator = new CompositeTileGenerator (plugin , this );
3033 }
3134
3235 public CompletableFuture <byte []> getTile (String worldName , int zoom , int tileX , int tileZ ) {
36+ // Route negative zoom levels to composite tile generator
37+ if (zoom < 0 && this .plugin .getConfig ().isEnableTilePyramids ()) {
38+ return this .getCompositeTile (worldName , zoom , tileX , tileZ );
39+ }
40+
41+ // For zoom >= 0, use base tile logic
42+ return this .getBaseTile (worldName , tileX , tileZ );
43+ }
44+
45+ /**
46+ * Get a composite tile at a negative zoom level.
47+ * Composite tiles combine multiple base tiles into one.
48+ */
49+ private CompletableFuture <byte []> getCompositeTile (String worldName , int zoom , int tileX , int tileZ ) {
3350 String cacheKey = TileCache .createKey (worldName , zoom , tileX , tileZ );
3451
35- // 1. Check memory cache first (fastest)
52+ // 1. Check memory cache first
3653 byte [] memoryCached = this .memoryCache .get (cacheKey );
3754 if (memoryCached != null ) {
3855 return CompletableFuture .completedFuture (memoryCached );
@@ -44,11 +61,71 @@ public CompletableFuture<byte[]> getTile(String worldName, int zoom, int tileX,
4461 return pending ;
4562 }
4663
47- // 3. Check disk cache if enabled
64+ // 3. Check disk cache
4865 if (this .plugin .getConfig ().isUseDiskCache ()) {
4966 byte [] diskCached = this .diskCache .get (worldName , zoom , tileX , tileZ );
5067 if (diskCached != null ) {
5168 long tileAge = this .diskCache .getTileAge (worldName , zoom , tileX , tileZ );
69+ // Use longer refresh interval for composite tiles (they're more expensive)
70+ long refreshInterval = this .plugin .getConfig ().getTileRefreshIntervalMs () * 2 ;
71+
72+ if (tileAge < refreshInterval ) {
73+ this .memoryCache .put (cacheKey , diskCached );
74+ return CompletableFuture .completedFuture (diskCached );
75+ }
76+
77+ // Check if any players are in the area covered by this composite tile
78+ int chunksPerAxis = this .compositeTileGenerator .getChunksPerAxis (zoom );
79+ int baseChunkX = tileX * chunksPerAxis ;
80+ int baseChunkZ = tileZ * chunksPerAxis ;
81+ World world = Universe .get ().getWorld (worldName );
82+
83+ if (world == null || !this .arePlayersInArea (world , baseChunkX , baseChunkZ , chunksPerAxis )) {
84+ this .memoryCache .put (cacheKey , diskCached );
85+ return CompletableFuture .completedFuture (diskCached );
86+ }
87+ }
88+ }
89+
90+ // 4. Generate composite tile
91+ CompletableFuture <byte []> future = this .compositeTileGenerator .generateCompositeTile (worldName , zoom , tileX , tileZ );
92+ this .pendingRequests .put (cacheKey , future );
93+ future .whenComplete ((data , ex ) -> {
94+ this .pendingRequests .remove (cacheKey );
95+ if (data != null && data .length > 0 && ex == null ) {
96+ this .memoryCache .put (cacheKey , data );
97+ if (this .plugin .getConfig ().isUseDiskCache ()) {
98+ this .diskCache .put (worldName , zoom , tileX , tileZ , data );
99+ }
100+ }
101+ });
102+ return future ;
103+ }
104+
105+ /**
106+ * Get a base tile (zoom level 0) for a single chunk.
107+ * This is called by the composite tile generator.
108+ */
109+ public CompletableFuture <byte []> getBaseTile (String worldName , int tileX , int tileZ ) {
110+ String cacheKey = TileCache .createKey (worldName , 0 , tileX , tileZ );
111+
112+ // 1. Check memory cache first (fastest)
113+ byte [] memoryCached = this .memoryCache .get (cacheKey );
114+ if (memoryCached != null ) {
115+ return CompletableFuture .completedFuture (memoryCached );
116+ }
117+
118+ // 2. Check if already generating
119+ CompletableFuture <byte []> pending = this .pendingRequests .get (cacheKey );
120+ if (pending != null ) {
121+ return pending ;
122+ }
123+
124+ // 3. Check disk cache if enabled
125+ if (this .plugin .getConfig ().isUseDiskCache ()) {
126+ byte [] diskCached = this .diskCache .get (worldName , 0 , tileX , tileZ );
127+ if (diskCached != null ) {
128+ long tileAge = this .diskCache .getTileAge (worldName , 0 , tileX , tileZ );
52129 long refreshInterval = this .plugin .getConfig ().getTileRefreshIntervalMs ();
53130
54131 // If tile is fresh enough, serve from disk
@@ -70,14 +147,14 @@ public CompletableFuture<byte[]> getTile(String worldName, int zoom, int tileX,
70147 }
71148
72149 // 4. Generate new tile
73- CompletableFuture <byte []> future = this .generateTile (worldName , zoom , tileX , tileZ );
150+ CompletableFuture <byte []> future = this .generateTile (worldName , 0 , tileX , tileZ );
74151 this .pendingRequests .put (cacheKey , future );
75152 future .whenComplete ((data , ex ) -> {
76153 this .pendingRequests .remove (cacheKey );
77154 if (data != null && data .length > 0 && ex == null ) {
78155 this .memoryCache .put (cacheKey , data );
79156 if (this .plugin .getConfig ().isUseDiskCache ()) {
80- this .diskCache .put (worldName , zoom , tileX , tileZ , data );
157+ this .diskCache .put (worldName , 0 , tileX , tileZ , data );
81158 }
82159 }
83160 });
@@ -109,6 +186,35 @@ private boolean arePlayersNearby(World world, int tileX, int tileZ) {
109186 return false ;
110187 }
111188
189+ /**
190+ * Check if any players are within the area covered by a composite tile.
191+ */
192+ private boolean arePlayersInArea (World world , int baseChunkX , int baseChunkZ , int chunksPerAxis ) {
193+ int radius = this .plugin .getConfig ().getTileRefreshRadius ();
194+
195+ for (PlayerRef playerRef : world .getPlayerRefs ()) {
196+ try {
197+ Transform transform = playerRef .getTransform ();
198+ if (transform == null ) continue ;
199+
200+ Vector3d pos = transform .getPosition ();
201+ int playerChunkX = ChunkUtil .chunkCoordinate ((int ) pos .x );
202+ int playerChunkZ = ChunkUtil .chunkCoordinate ((int ) pos .z );
203+
204+ // Check if player is within the area (with buffer for refresh radius)
205+ if (playerChunkX >= baseChunkX - radius &&
206+ playerChunkX < baseChunkX + chunksPerAxis + radius &&
207+ playerChunkZ >= baseChunkZ - radius &&
208+ playerChunkZ < baseChunkZ + chunksPerAxis + radius ) {
209+ return true ;
210+ }
211+ } catch (Exception e ) {
212+ // Skip this player
213+ }
214+ }
215+ return false ;
216+ }
217+
112218 private CompletableFuture <byte []> generateTile (String worldName , int zoom , int tileX , int tileZ ) {
113219 World world = Universe .get ().getWorld (worldName );
114220 if (world == null ) {
0 commit comments