def pred_tile()

in server.py [0:0]


def pred_tile():
    bottle.response.content_type = 'application/json'
    data = bottle.request.json
    current_session = SESSION_HANDLER.get_session(bottle.request.session.id)

    current_session.add_entry(data) # record this interaction

    # Inputs
    geom = data["polygon"]
    class_list = data["classes"]
    name_list = [item["name"] for item in class_list]
    color_list = [item["color"] for item in class_list]
    dataset = data["dataset"]
    zone_layer_name = data["zoneLayerName"]
    model_idx = data["modelIdx"]

    if dataset not in DATALOADERS:
        raise ValueError("Dataset doesn't seem to be valid, do the datasets in js/tile_layers.js correspond to those in TileLayers.py")    
    else:
        current_data_loader = DATALOADERS[dataset]

    try:
        input_raster = current_data_loader.get_data_from_geometry(geom["geometry"])
        shape_area = get_area_from_geometry(geom["geometry"])
    except NotImplementedError as e: # Example of how to handle errors from the rest of the server
        bottle.response.status = 400
        return json.dumps({"error": "Cannot currently download imagery with 'Basemap' based datasets"})
    
    output_raster = current_session.pred_tile(input_raster)
    if output_raster.shape[2] > len(color_list):
       LOGGER.warning("The number of output channels is larger than the given color list, cropping output to number of colors (you probably don't want this to happen")
       output_raster.data = output_raster.data[:,:,:len(color_list)]
    
    output_hard = output_raster.data.argmax(axis=2)
    nodata_mask = np.sum(input_raster.data == 0, axis=2) == input_raster.shape[2]
    output_hard[nodata_mask] = 255
    class_vals, class_counts = np.unique(output_hard[~nodata_mask], return_counts=True)

    img_hard = class_prediction_to_img(output_raster.data, True, color_list)
    img_hard = cv2.cvtColor(img_hard, cv2.COLOR_RGB2BGRA)
    img_hard[nodata_mask] = [0,0,0,0]

    # replace the output predictions with our image data because we are too lazy to make a new InMemoryRaster
    output_raster.data = img_hard
    output_raster.shape = img_hard.shape

    warped_output_raster = warp_data_to_3857(output_raster) # warp output to 3857
    cropped_warped_output_raster = crop_data_by_geometry(warped_output_raster, geom["geometry"], "epsg:4326") # crop to the desired shape
    img_hard = cropped_warped_output_raster.data

    tmp_id = get_random_string(8)
    cv2.imwrite("tmp/downloads/%s.png" % (tmp_id), img_hard)
    data["downloadPNG"] = "tmp/downloads/%s.png" % (tmp_id)

    new_profile = {}
    new_profile['driver'] = 'GTiff'
    new_profile['dtype'] = 'uint8'
    new_profile['compress'] = "lzw"
    new_profile['count'] = 1
    new_profile['transform'] = output_raster.transform
    new_profile['height'] = output_hard.shape[0] 
    new_profile['width'] = output_hard.shape[1]
    new_profile['nodata'] = 255
    new_profile['crs'] = output_raster.crs
    with rasterio.open("tmp/downloads/%s.tif" % (tmp_id), 'w', **new_profile) as f:
        f.write(output_hard.astype(np.uint8), 1)
    data["downloadTIFF"] = "tmp/downloads/%s.tif" % (tmp_id)


    data["classStatistics"] = []

    f = open("tmp/downloads/%s.txt" % (tmp_id), "w")
    f.write("Class id\tClass name\tPercent area\tArea (km^2)\n")
    for i in range(len(class_vals)):
        pct_area = (class_counts[i] / np.sum(class_counts))
        if shape_area is not None:
            real_area = shape_area * pct_area
        else:
            real_area = -1
        f.write("%d\t%s\t%0.4f%%\t%0.4f\n" % (class_vals[i], name_list[class_vals[i]], pct_area*100, real_area))
        data["classStatistics"].append({
            "Class ID": int(class_vals[i]),
            "Class Name": name_list[class_vals[i]],
            "Percent Area": float(pct_area),
            "Area (km2)": float(real_area)
        })
    f.close()
    data["downloadStatistics"] = "tmp/downloads/%s.txt" % (tmp_id)

    bottle.response.status = 200
    return json.dumps(data)