docs: @Api
  	public List<RasterTile> paint(TiledRasterLayerServiceState tileServiceState, CoordinateReferenceSystem boundsCrs,
  			Envelope bounds, double scale) throws GeomajasException {
  		try {
  			CrsTransform layerToMap = geoService.getCrsTransform(tileServiceState.getCrs(), boundsCrs);
  			CrsTransform mapToLayer = geoService.getCrsTransform(boundsCrs, tileServiceState.getCrs());
  
  			bounds = clipBounds(tileServiceState, bounds);
  			if (bounds.isNull()) {
  				return new ArrayList<RasterTile>();
  			}
  
  			// find the center of the map in map coordinates (positive y-axis)
  			Coordinate boundsCenter = new Coordinate((bounds.getMinX() + bounds.getMaxX()) / 2,
  					(bounds.getMinY() + bounds.getMaxY()) / 2);
  
  			// find zoomlevel
  			// scale in pix/m should just above the given scale so we have at least one
  			// screen pixel per google pixel ! (otherwise text unreadable)
  			int zoomLevel = getBestZoomLevelForScaleInPixPerMeter(tileServiceState, mapToLayer, boundsCenter, scale);
  			log.debug("zoomLevel={}", zoomLevel);
  
  			// find the google indices for the center
  			// google indices determine the row and column of the 256x256 image
  			// in the big google square for the given zoom zoomLevel
  			// the resulting indices are floating point values as the center
  			// is not coincident with an image corner !!!!
  			Coordinate indicesCenter;
  			indicesCenter = getTileIndicesFromMap(mapToLayer, boundsCenter, zoomLevel);
  
  			// Calculate the width in map units of the image that contains the
  			// center
  			Coordinate indicesUpperLeft = new Coordinate(Math.floor(indicesCenter.x), Math.floor(indicesCenter.y));
  			Coordinate indicesLowerRight = new Coordinate(indicesUpperLeft.x + 1, indicesUpperLeft.y + 1);
  			Coordinate mapUpperLeft = getMapFromTileIndices(layerToMap, indicesUpperLeft, zoomLevel);
  			Coordinate mapLowerRight =
  					getMapFromTileIndices(layerToMap, indicesLowerRight, zoomLevel);
  			double width = Math.abs(mapLowerRight.x - mapUpperLeft.x);
  			if (0 == width) {
  				width = 1.0;
  			}
  			double height = Math.abs(mapLowerRight.y - mapUpperLeft.y);
  			if (0 == height) {
  				height = 1.0;
  			}
  
  			// Calculate the position and indices of the center image corner
  			// in map space
  			double xCenter = boundsCenter.x - (indicesCenter.x - indicesUpperLeft.x) * width;
  			double yCenter = boundsCenter.y + (indicesCenter.y - indicesUpperLeft.y) * height;
  			int iCenter = (int) indicesUpperLeft.x;
  			int jCenter = (int) indicesUpperLeft.y;
  
  			// Calculate the position and indices of the upper left image corner
  			// that just falls off the screen
  			double xMin = xCenter;
  			int iMin = iCenter;
  			while (xMin > bounds.getMinX() && iMin > 0) {
  				xMin -= width;
  				iMin--;
  			}
  			double yMax = yCenter;
  			int jMin = jCenter;
  			while (yMax < bounds.getMaxY() && jMin > 0) {
  				yMax += height;
  				jMin--;
  			}
  			// Calculate the indices of the lower right corner
  			// that just falls off the screen
  			int levelMax = POWERS_OF_TWO[zoomLevel] - 1;
  			double xMax = xCenter;
  			int iMax = iCenter;
  			while (xMax < bounds.getMaxX() && iMax <= levelMax) {
  				xMax += width;
  				iMax++;
  			}
  			double yMin = yCenter;
  			int jMax = jCenter;
  			while (yMin > bounds.getMinY() && jMax <= levelMax) {
  				yMin -= height;
  				jMax++;
  			}
  			Coordinate upperLeft = new Coordinate(xMin, yMax);
  
  			// calculate the images
  			List<RasterTile> result = new ArrayList<RasterTile>();
  			if (log.isDebugEnabled()) {
  				log.debug("bounds =" + bounds);
  				log.debug("tilebounds " + xMin + ", " + xMax + ", " + yMin + ", " + yMax);
  				log.debug("indices " + iMin + ", " + iMax + ", " + jMin + ", " + jMax);
  			}
  			int xScreenUpperLeft = (int) Math.round(upperLeft.x * scale);
  			int yScreenUpperLeft = (int) Math.round(upperLeft.y * scale);
  			int screenWidth = (int) Math.round(scale * width);
  			int screenHeight = (int) Math.round(scale * height);
  			for (int i = iMin; i < iMax; i++) {
  				for (int j = jMin; j < jMax; j++) {
  					// Using screen coordinates:
  					int x = xScreenUpperLeft + (i - iMin) * screenWidth;
  					int y = yScreenUpperLeft - (j - jMin) * screenHeight;
  
  					RasterTile image = new RasterTile(new Bbox(x, -y, screenWidth, screenHeight),
  							tileServiceState.getId() + "." + zoomLevel + "." + i + "," + j);
  					image.setCode(new TileCode(zoomLevel, i, j));
  					String url = tileServiceState.getUrlSelectionStrategy().next();
  					url = url.replace("${level}", Integer.toString(zoomLevel));
  					url = url.replace("${x}", Integer.toString(i));
  					url = url.replace("${y}", Integer.toString(j));
  					image.setUrl(url);
  					log.debug("adding image {}", image);
  					result.add(image);
  				}
  			}
  			return result;
  		} catch (TransformException e) {
  			throw new GeomajasException(e, ExceptionCode.RENDER_TRANSFORMATION_FAILED);
  		}
  	}