geospatiale/files/png/png_interpolation.js

//Initialise functions
{
	if (!global.GeoPNG)
		/**
		 * Analogous to a GeoTIFF file format, but in PNG form for easier editing. Single variable. Part of Geospatiale III.
		 *
		 * @namespace GeoPNG
		 */
		global.GeoPNG = {};
	
	/**
	 * Dasymetrically blurs border regions where ushc regions are artificial/statistical artefacts.
	 * @alias GeoPNG.dasymetricBlur
	 * 
	 * @param {Object} [arg0_options]
	 *  @param {number[]} [arg0_options.mask_data]
	 *  @param {Float64Array|number[]} [arg0_options.pop_data] - Data referencing population, alternatively any utility in which low values contribute to higher spread of the target variable.
	 *  @param {Float64Array|number[]} [arg0_options.target_data]
	 *  
	 *  @param {number} [arg0_options.height=2160]
	 *  @param {number} [arg0_options.width=4320]
	 *  
	 *  @param {number} [arg0_options.radius=64] - The default brush radius/tolerance.
	 */
	GeoPNG.dasymetricBlur = function (arg0_options) {
		//Convert from parameters
		let options = (arg0_options) ? arg0_options : {};
		
		//Initialise options
		options.height = Math.returnSafeNumber(options.height, 2160);
		options.width = Math.returnSafeNumber(options.width, 4320);
		
		options.radius = Math.returnSafeNumber(options.radius, 64);
		
		//Declare local instance variables
		let blurPass = (in_A, in_B, r) => {
			let out_A = new Float32Array(N), out_B = new Float32Array(N);
			let temp_A = new Float32Array(N), temp_B = new Float32Array(N);
			
			for (let y = 0; y < height; y++) {
				let offset = y * width, sum_A = 0, sum_B = 0;
				for (let x = 0; x <= r && x < width; x++) { sum_A += in_A[offset+x]; sum_B += in_B[offset+x]; }
				for (let x = 0; x < width; x++) {
					temp_A[offset+x] = sum_A; temp_B[offset+x] = sum_B;
					if (x + r + 1 < width) { sum_A += in_A[offset+x+r+1]; sum_B += in_B[offset+x+r+1]; }
					if (x - r >= 0) { sum_A -= in_A[offset+x-r]; sum_B -= in_B[offset+x-r]; }
				}
			}
			for (let x = 0; x < width; x++) {
				let sum_A = 0, sum_B = 0;
				for (let y = 0; y <= r && y < height; y++) { let idx = y*width+x; sum_A += temp_A[idx]; sum_B += temp_B[idx]; }
				for (let y = 0; y < height; y++) {
					let idx = y*width+x; out_A[idx] = sum_A; out_B[idx] = sum_B;
					if (y + r + 1 < height) { let n_idx = (y+r+1)*width+x; sum_A += temp_A[n_idx]; sum_B += temp_B[n_idx]; }
					if (y - r >= 0) { let p_idx = (y-r)*width+x; sum_A -= temp_A[p_idx]; sum_B -= temp_B[p_idx]; }
				}
			}
			return {A: out_A, B: out_B};
		};
		let height = options.height;
		let N = options.width*options.height;
		let mask_data = options.mask_data;
		let radius = options.radius;
		let pop_data = options.pop_data;
		let target_data = options.target_data;
		let width = options.width;
		
		// 1. Precise Border Detection & Distance Transform
		let dist = new Float32Array(N).fill(99999);
		for (let y = 0; y < height; y++) {
			for (let x = 0; x < width; x++) {
				let idx = y * width + x;
				let b_idx = idx * 4;
				let c = (mask_data[b_idx] << 16) | (mask_data[b_idx+1] << 8) | mask_data[b_idx+2];
				if (c === 0) continue;
				
				let is_border = false;
				let neighbors = [
					(y > 0) ? idx - width : -1,
					(y < height - 1) ? idx + width : -1,
					(x > 0) ? idx - 1 : -1,
					(x < width - 1) ? idx + 1 : -1
				];
				
				for (let n of neighbors) {
					if (n !== -1) {
						let n_byte = n * 4;
						let nc = (mask_data[n_byte] << 16) | (mask_data[n_byte+1] << 8) | mask_data[n_byte+2];
						if (nc !== c && nc !== 0) {
							is_border = true;
							break;
						}
					}
				}
				if (is_border) dist[idx] = 0;
			}
		}
		
		let d1 = 3, d2 = 4;
		for (let y = 0; y < height; y++) {
			for (let x = 0; x < width; x++) {
				let idx = y * width + x;
				if (x > 0) dist[idx] = Math.min(dist[idx], dist[idx - 1] + d1);
				if (y > 0) dist[idx] = Math.min(dist[idx], dist[idx - width] + d1);
				if (x > 0 && y > 0) dist[idx] = Math.min(dist[idx], dist[idx - width - 1] + d2);
				if (x < width - 1 && y > 0) dist[idx] = Math.min(dist[idx], dist[idx - width + 1] + d2);
			}
		}
		for (let y = height - 1; y >= 0; y--) {
			for (let x = width - 1; x >= 0; x--) {
				let idx = y * width + x;
				if (x < width - 1) dist[idx] = Math.min(dist[idx], dist[idx + 1] + d1);
				if (y < height - 1) dist[idx] = Math.min(dist[idx], dist[idx + width] + d1);
				if (x < width - 1 && y < height - 1) dist[idx] = Math.min(dist[idx], dist[idx + width + 1] + d2);
				if (x > 0 && y < height - 1) dist[idx] = Math.min(dist[idx], dist[idx + width - 1] + d2);
			}
		}
		for (let i = 0; i < N; i++) dist[i] /= 3.0;
		
		// 2. Uniform Double-Box Blur (The "Brush")
		let A = new Float32Array(N);
		let B = new Float32Array(N);
		for (let i = 0; i < N; i++) {
			if (target_data[i] > 0) {
				A[i] = target_data[i];
				B[i] = 1.0;
			}
		}
		
		let pass1 = blurPass(A, B, radius);
		let pass2 = blurPass(pass1.A, pass1.B, radius);
		
		// 3. Apply the Smart Brush (Interpolate Alpha via Distance & Population)
		let output_data = new Float32Array(N);
		
		let log_min = Math.log10(5);
		let log_max = Math.log10(5000);
		
		for (let i = 0; i < N; i++) {
			if (target_data[i] === 0) {
				output_data[i] = 0;
				continue;
			}
			
			let d = dist[i];
			if (d >= radius) {
				output_data[i] = target_data[i];
			} else {
				let V_raw = target_data[i];
				let V_blur = (pass2.B[i] > 0) ? (pass2.A[i] / pass2.B[i]) : V_raw;
				
				let p = pop_data[i];
				let log_p = Math.log10(Math.max(1, p));
				
				let a_pop = (log_p - log_min) / (log_max - log_min);
				a_pop = Math.max(0, Math.min(1, a_pop));
				
				let a_dist = d / radius;
				
				// Raw factor approaches 1 in cities OR inland. Approaches 0 strictly near borders in wilderness.
				let raw_factor = Math.max(a_pop, a_dist);
				raw_factor = raw_factor * raw_factor * (3 - 2 * raw_factor); // Smoothstep
				
				output_data[i] = (V_raw * raw_factor) + (V_blur * (1.0 - raw_factor));
			}
		}
		
		//Return statement
		return output_data;
	};
}