Random Map Generator - Creating areas on a grid - php

I'm developing a game which requires a randomly generated map. The map is going to be, effectively, a giant grid containing mostly emptiness with objects scattered about, grouped in regions. The idea is that I first generate these regions, then individual classes for each region type handle the generation of the map within the regions.
What I'm currently trying to do is generate these regions randomly on a grid. The regions will always be rectangles or squares and of any size, and the grid itself could also be of any size (again, either rectangular or square).
So to simplify all this down, I'm trying to generate randomly proportioned rectangles on a grid of arbitrary size. For the record, when I say arbitrary, I mean seriously large - we could be looking at sizes as high as 100,000 x 100,000, potentially. Regions are probably going to be a maximum of 100x100, but again, the script should be able to cope with any size.
Just to throw some more constraints out there, these regions cannot overlap. Ultimately, both the regions, and the objects generated within each region will be put into a MySQL database.
I initially thought that using a 2 dimensional array to represent the entire grid and simply marking spaces as taken within that would be fine, and it was in initial tests for small sized grids. I found for very large grids, Php complained about exceeding the memory limit and would not execute the script.
Another approach I thought about taking but haven't tried is inserting regions into the database as they are created and then running a query on the database to see if an area is empty or not every time it is required. I'm confident I could make this work, but the amount of database queries would be huge. I can reduce the number of tests required by eliminating certain squares that I can know for sure to be unusable via purely mathematical means, but for large grids we're still talking a massive amount of queries. This would make the script really, really time intensive and I can't help but feel that it's unnecessary. I'd prefer to do things another way, if possible.
The last method I thought of is purely mathematical, and would be perfect if I could make it work. I used only two variables - maximum x and maximum y to store the highest value on each axis. When testing to create a region I simply checked if the new region's top left coordinate was lower than both the maximum x and y - if it was, the space could not be used. If either one was higher, there was no issue and the region was created. Some of you might spot the problem here I imagine, but to cut a long story short there's certain situations where this system would judge an empty space to be unusable, so masses of random empty blocks appeared when I ran the script.
So, I'm stuck. I can't help but feel that there's a good mathematical solution to this that uses a fixed number of variables, but I can't see it. Failing that, does anyone know of another way to achieve what I'm looking for?

Have a read through this, maybe it will help. I haven't tested it, but I have commented my intentions so you should be able to come up with something based on it.
//create some globals we will need
$region_id = 1;
$current_x = 0;
$current_y = 0;
$squares = array();
//Create the base grid
$grid = array(GRID_WIDTH);
foreach($grid as $a)
{
$a = array(GRID_HEIGHT);
$a = array_fill(0, count($a), 0);
}
//$grid[0][0] will be the top left square
//Iterates over the grid until it is full
while($current_x != (-1) && $current_y != (-1))
{
//We just need to set these counters back to their minimums
$max_x = $current_x;
$max_y = $current_y;
//Lets see how many spaces we have between current x and end of the row
//$max_x will be the maxiumum newX2
for($x = $current_x; $x<GRID_WIDTH; $x++)
{
if($grid[$x][$current_y]==0)
{
$max_x = $x;
}
}
//Lets create a width between 1 and the max left in the row
$newX1 = $current_x;
$newX2 = rand($current_x, $max_x)
//Now lets see how tall we can make it by going down the y until we hit a square or the end.. do for each x
for($x = $newX1; $x<newX2;$x++)
{
for($y = $current_y; $y<GRID_HEIGHT; $y++)
{
if($grid[$x][$y]==0)
{
//We just need the maxiumum y we can have based on our already determined width
//Since this distance has to be smallest possible distance
//We only let max_y get larger on the first iteration of x
if($y>$max_y && $x==$newX1)
{
$max_y = $y;
}
}
}
}
//lets create a height bettwen 1 and the max
$newY1 = $current_y;
$newY2 = rand($current_y, $max_y);
//create a spacewith these values
occupySpace($newX1, $newX2, $newY1, $newY2, $grid, $region_id);
$squares[] = array($newX1, $newX2, $newY1, $newY2);
//Iterate over the grid looking for an empty space
//First lets set current_x and current_y to -1. We will use this to evalute done
$current_x = -1;
$current_y = -1;
for($x = 0; $x<GRID_WIDTH; $x++)
{
for($y = 0; $y<GRID_HEIGHT; $y++)
{
//Looking for any space that hasn't been occupied, if one is found break both for loops
if($grid[$x][$y]==0)
{
$current_x = $x;
$current_y = $y;
break 2;
}
}
}
}
//fills all spaces in the region with the current region id and increments region id
function occupySpace($x1, $x2, $y1, $y2, &$grid, &$region_id)
{
for($x=$x1; $x<$x2; $x++)
{
for($y=$y1; $y<$y2; $y++)
{
$grid[$x][$y] = $region_id;
}
}
$region_id += 1;
}
?>
Update: This solution didn't originally keep track the 'regions' that were created. Now they exist in array $squares.
PS - If this is going to be a big part of your script/business I would consider writing some classes for Grid and region,

there is no need to save every bit of information into the db. For every rectangle you only need top/left-coordinates + width/height.
then you should be able get the maximum available space for a given coordinate with with one query (e.g. getting minimum-x which is bigger than your-x and also lies within the needed y-space)
Update:
For max-width you can use:
SELECT `x` - :x AS `maxWidth`
FROM `test`
WHERE `x` > :x
AND (`y` > :y OR `y` + `h` > :y)
ORDER BY `y`, `y` + `h`
LIMIT 1
and max-height:
SELECT `y` - :y AS `maxHeight`
FROM `test`
WHERE `y` > :y
AND (`x` > :x OR `x` + `w` > :x)
ORDER BY `x`, `x` + `w`
LIMIT 1

Related

Labels created with PHP FPDF Library slide down page

I have created an FPDF PDF document that puts an image on label paper which has 9 rows of labels (PHP code below)
There is no space/gap between each row of labels, and the next row of labels start immediately after the previous row.
### THE ISSUE: ###
When the labels are printed, the image displayed on each label moves down slightly from the top of each label, causing the bottom row of labels (9th row) to be significantly different to the 1st couple of rows.
I need to add additional content to each label, and if this issue continues, some of this content is going to get cut off.
I don't understand why this is happening, and can't see anything obvious in the code. Can anyone here spot what I'm doing wrong?
My code.....
use Fpdf\Fpdf as FPDF;
$current_x_position = 0;
$current_y_position = 0;
$total_y_per_page = 9;
$total_x_per_page = 3;
$pdf = new FPDF();
$pdf->SetMargins(0,0);
$pdf->SetAutoPageBreak(false);
$pdf->AddPage();
for($qty = 1; $qty <= 10; $qty++) {
label($current_x_position, $current_y_position, $pdf);
$current_y_position++;
if($current_y_position == $total_y_per_page) {
$current_x_position++;
$current_y_position = 0;
if($current_x_position == $total_x_per_page) {
$current_x_position = 0;
$current_y_position = 0;
$pdf->AddPage('P');
}
}
}
$pdf->Output();
function label($current_x_position, $current_y_position, $pdf) {
$left_margin = 7;
$top_margin = 15;
$label_width = 66;
$label_height = 30;
$current_x_position = $left_margin + ($label_width * $current_x_position);
$current_y_position = $top_margin + ($label_height * $current_y_position);
$pdf->Image('image.png', $current_x_position, $current_y_position+=1, $label_width, 10, 'png');
return;
}
?>
I think the problem stems from how x is being handled. It should be distinct from y. Why? Because x needs to be reset after every 3rd label is printed. Since it is only tested and incremented after 9 rows are printed, it will surely cause slueing. (Maybe that's an old-timey expression, used to indicate a program causes a new line because it is already past the column in which you want to print). Suggest you need to test x after every label print, so it can be reset to 0 when it reaches total_x_per_page (which I think really means total_x_per_row). The x test needs to be independent of the y test; it needs to be before the y test because you the columns will be complete before the rows.
In pseudo-ish code:
print a label
if the row is complete reset x, otherwise increase x
if the page is complete, reset y and x.
My experience with Fpdf is nil. My experience with (fighting) labels goes back decades :)

Graphing trigonometric functions

I've come across something that I'd like to accomplish without using any frameworks or other graphing tools that are out there on the internet, which is graphing trigonometric functions using only PHP and if needed, SQL. I'm aware of the GD library, but none of the functions were helpful. I wrote a small script, although it doesn't really work either. My goal is to do the following:
Allow for trig function name, starting value and end value to be added as parameters.
Check whether a function is cos, tan or sin.
Loop through all values given starting and end values in degrees, and convert to radians.
"Add" all values to an array, if needed, and graph the function given the points.
After looping through all the values of the function, what needs to be done to graph the function? Do the values have to be in a seperate array? What functions need to be used for graphing?
<?php
header("Content-type: image/png");
function graphFunction($function, $startDegree, $endDegree)
{
$functionList = array('cos', 'sin', 'tan');
if (strtolower($function) == 'cos')
{
$cosValues = array();
for ($c = $startDegree; $c < $endDegree; $c++)
{
array_push($cosValues, cos(deg2rad($c)));
$graph = imagecreatetruecolor(500,250);
$col_poly = imagecolorallocate($graph, 255, 255, 255);
imagepolygon($graph, [the cosine values] , 34, $col_poly);
imagepng($graph);
imagedestroy($graph);
}
}
}
echo graphFunction('cos', 0, 360);
?>
This is supposed to be a sample function, so no need to be criticizing the useless control structure as there are ways to store many things in a database, whatsoever. I hope for some feedback, and hopefully it is possible with PHP.
You can do this with PHP. I just tried with the following inside your if statement, and it seems to work.
$height = 250;
$offset = $height/2;
$graph = imagecreatetruecolor(500, $height);
$col_poly = imagecolorallocate($graph, 255, 255, 255);
for ($c = $startDegree; $c < $endDegree; $c++)
{
$this_x = $c;
$next_x = $this_x + 1;
$this_y = cos(deg2rad($this_x)) * $offset + $offset;
$next_y = cos(deg2rad($next_x)) * $offset + $offset;
imageline($graph, $this_x, $this_y, $next_x, $next_y, $col_poly);
}
imagepng($graph);
imagedestroy($graph);
So what I did was define a graph height, because the y coordinates of the points in the graph need to be set to they fit inside. Then inside the for loop I just take the current degree value as my x coordinate, calculate the y coordinate for it, then the same for the next degree value, and then I draw a line between those two points.
It's a bit cludgy, so I'm sure you'll wan to clean it up a bit. Also, you need to think about the width of your graph as well (what I've posted here will stop at 500 px wide, so if you want to graph a wider range, or if you want to plot 300 - 600 degrees it will just start halfway into your graphing area.
You also defined your image and colours inside the for loop, which I changed so you're not recreating an image resource each step and losing all previous data.
Anyway, it was just an example function, right? But using your degree values as x coordinates (adjusted to your graphing area sise), computing y coordinates based on the chosen function, the degree values and your graphing area size and then drawing lines between the points should work.

Proper way of standardizing images

I am building a web application that will use the Amazon Product Advertising API. The problem is that the API returns images of different sizes and aspect ratios for each product. If I resize all images to a static width/height, then some of them look odd due to the change in ratio. The images will be laid out in rows of four each. I was thinking that I will only make the width of the images the same while keeping the aspect ratio the same and then having some sort of max threshold for the height just in case the API returns some oddly sized image.
But, I wanted to see if people here have ran into this before and what their thoughts on this design problem are?
What a co-incidence. I was facing a similar problem and this is how I've decided to move forward. It might not be the best solution, but it works for me.
First I get the original height and width of the image
list($width, $height) = getimagesize("path to image");
Then I find out the greatest common divisor for the two and store the width and height ratios in variables, such as $wr and $hr
Then check for image orientation is made (horizontal or vertical) by comparing $wr > $hr for horizontal orientation and $hr > $wr for vertical
If horizontal, I make sure the thumb size does not exceed a certain value, say 120px and make the height corresponding to 120px based on aspect ratio. The same is done if the orientation is vertical.
I came across the same problem trying to standardize logos. At first, I thought that I could choose a standard area and resize all the images to that area, but there are two problems with that strategy. First, you have to set limits for height and width, so sometimes you end up images with a smaller area because they are very wide or tall. Secondly, that might make your layout look ugly. In my case I have a header on top, an image and then text below. I realized that a very wide image would leave a big gap between the header and the text. Because of that, I needed a solution that would give preference to width. What I mean by that is that the image can change a lot in width without changing the height a lot. Different applications may have different requirements, but I think my solution allows for any situation such as a preference for height as well:
function width_to_height($width, $slope, $icpt){
if ($width == 0){
return FALSE;
}
return floor(($icpt + sqrt(pow($icpt, 2) + 4*$slope*pow($width, 2)))/(2*$width));
}
function ratio_to_height($ratio, $slope, $icpt){
if ($ratio == 0){
return FALSE;
}
$area = $ratio*$slope + $icpt;
return floor(sqrt($area/$ratio));
}
function calc_dims($width, $height, $max_w=168, $max_h=100){
$slope = 2500;
$icpt = 6000;
$ratio = $width/$height;
$max_ratio = $max_w/$this->width_to_height($max_w, $slope, $icpt);
if ($ratio > $max_ratio){
$ht = floor($max_w/$ratio);
return array('width' => $max_w, 'height' => $ht);
}
$ht = $this->ratio_to_height($ratio, $slope, $icpt);
if ($ht > $max_h){
$wd = floor($max_h*$ratio);
return array('width' => $wd, 'height' => $max_h);
}
$wd = floor($ht*$ratio);
return array('width' => $wd, 'height' => $ht);
}
The main function is calc_dims() which calls the other two functions. Since I almost always work with classes, these functions are called with the "this->" operator. If you don't use classes, you can simply delete the operator.
You can see that I hard-coded some variables. (In my real life application, these are called from a configuration file.) $max_w and $max_h are pretty self explanatory as the maximum height and width of the images. However $slope and $icpt may be a little more difficult to understand. As you can see in the ratio_to_height() function, $slope and $icpt (intercept) are components in a linear relationship between area and the aspect ratio of the image.
You can use the values that I provide ($slope = 2500; $icpt = 6000), or you can calculate your own. I thought of automating the process a little more, but since the parameters are highly subjective, it didn't seem very practical. In order to calculate them, it's necessary to define areas for two different instances of the aspect ratios, where the ratio is $width/$height. For example, when the ratio is 1 ($width = $height), you might want the area to be 8000 pixels^2, and when the ratio is 2 ($width = 2*$height) the area could be 12000 pixels^2. With these values we have:
$A1 = 8000;
$A2 = 12000;
$r1 = 1;
$r2 = 2;
You can calculate slope and intercept as follows:
$slope = ($A2 - $A1)/($r2 - $r1);
$icpt = $A1 - $slope*$r1;

find memory usage

I have a table with 150x150 cells which each have a colored background and a small image/symbol in them. Additionally each cell can have zero or more of it's borders set also. The user can change and add borders and change the cell color and cell. This all works pretty well using jquery and it is just a plain html table.
At the end of the user experience I am making a pdf of this table for them to download. This is a big job and takes a while, which is not my main concern now. jQuery gathers the table data in an array and sends it to php to recreate it as an image using the gd library.
So for each cell I was drawing a rectangle of the correct color on a large image, loading the symbol image and resampling it on to the large image, drawing the borders and imagedestroy the symbol image, it worked but took 1 minute. I changed my strategy to make a small colored rectangke impose the image on it and cache it in an array to be quickly used again. That sped my time up and brought it down to 30ish seconds, but now I am exhausting memory.
I am breaking the table down into 50 cell blocks so each fits on a page, each block is made into an image and saved to disk, the next is made and saved, etc. Each time the gd image is destroyed. Then all the blocks are inserted into the pdf.
So after all that, my question is how do I figure out where the memory is being used so I can try to free it up? I have posted the main function I think is causing the issue below. In my test there are up to 30 different symbols/colors images that are 25pxX25px, these are the images that are cached in an array.
I hope I have provided enough info and my problem is clear enough.
Thank you for your time,
Todd
//make one "block" of stitches returns image file name.
//function makeStitchChartBlock($img, $startX, $startY, $endX, $endY, $caption, $brand){
function makeStitchChartBlock($stitchChartArray, $startX, $startY, $endX, $endY, $caption, $brand,$blockNumber){
global $threadColours;
$stitchCache=array();
$saveTo = 'result'.$blockNumber.'.jpeg';
// calculate size of block
$numRows=($endY-$startY);
$numColumns=($endX-$startX);
$heightOfBlock = $numRows*SYMBOL_SIZE; //in pixels --- used to determine size of image to make for block
$widthOfBlock = $numColumns*SYMBOL_SIZE; //in pixels
//----plus any extra for captions grid lines
$heightOfBlock += (($endY-$startY)+1); //each stitch has a grid line before it and the last one also has on after it
$widthOfBlock += (($endX-$startX)+1);
// create image size of block to put stitches in
$newBlockImage = imagecreatetruecolor($widthOfBlock,$heightOfBlock);
$backStitchColor = imagecolorallocate($newBlockImage, 0, 0, 0);
// insert caption????
//draw grid lines
//$newBlockImage = addGridToImage($newBlockImage);
//keep track of where to start next cell top left
$blockX=0;
$blockY=0;
for($y = $startY; $y < $endY; $y++){ //for each pixel in height, move down 1 "row" each iteration
//echo "<tr>";
for($x = $startX; $x < $endX; $x++){ // "draws" a row (for each y pixel)
//rgb(75, 90, 60)
//x and y are column and row #'s
list($r, $g, $b) = getRGBs($stitchChartArray[$y][$x][0]); //get the rgb values for the cell
$stitchColor = imagecolorallocate($newBlockImage, $r, $g, $b);
//calculate x & y start positons
$stitchStartX=($blockX*SYMBOL_SIZE)+$blockX+1; //account for each previous stitch and the grid line, then add one for new grid line
$stitchStartY=($blockY*SYMBOL_SIZE)+$blockY+1;
$stitchEndX=$stitchStartX+(SYMBOL_SIZE);
$stitchEndY=$stitchStartY+(SYMBOL_SIZE);
/* make a symbol cell image with/without color and save it in the cache */
if(!isset($stitchCache[$r][$g][$b]))
{
//create new image
$stitchCache[$r][$g][$b] = imagecreatetruecolor(SYMBOL_SIZE,SYMBOL_SIZE);
$stitchCacheColor = imagecolorallocate($stitchCache[$r][$g][$b], $r, $g, $b);
//draw colored rectangle
imagefilledrectangle($stitchCache[$r][$g][$b], 0, 0, SYMBOL_SIZE-1, SYMBOL_SIZE-1, $stitchCacheColor);
//add the symbol
$symbolFile=$stitchChartArray[$y][$x][1];
if($symbolFile){
$symbolImage = imagecreatefrompng($symbolFile);
imagecopyresampled ($stitchCache[$r][$g][$b],$symbolImage,0,0,0,0,SYMBOL_SIZE-1,SYMBOL_SIZE-1,imagesx($symbolImage), imagesy($symbolImage) );
imagedestroy($symbolImage);
}
}
//add image from cache to the block image
imagecopyresampled ($newBlockImage,$stitchCache[$r][$g][$b],$stitchStartX, $stitchStartY,0,0,SYMBOL_SIZE,SYMBOL_SIZE,SYMBOL_SIZE,SYMBOL_SIZE);
//add the backstitch lines(borders)
if($stitchChartArray[$y][$x][2]>1) //top
{
imagefilledrectangle($newBlockImage, $stitchStartX, $stitchStartY, $stitchEndX, $stitchStartY+1, $backStitchColor);
}
if($stitchChartArray[$y][$x][3]>1) //right
{
imagefilledrectangle($newBlockImage, $stitchEndX-1, $stitchStartY, $stitchEndX, $stitchEndY, $backStitchColor);
}
if($stitchChartArray[$y][$x][4]>1) //bottom
{
imagefilledrectangle($newBlockImage, $stitchStartX, $stitchEndY-1, $stitchEndX, $stitchEndY, $backStitchColor);
}
if($stitchChartArray[$y][$x][5]>1) //left
{
imagefilledrectangle($newBlockImage, $stitchStartX, $stitchStartY, $stitchStartX+1, $stitchEndY, $backStitchColor);
}
//advance x position
$blockX++;
}
//advance y position
//reset x
$blockX=0;
$blockY++;
}
imagejpeg($newBlockImage, $saveTo);
imagedestroy($newBlockImage);
//dump stitch cache
foreach($stitchCache as $r)
{
foreach($r as $g)
{
foreach($g as $b=>$data)
{
imagedestroy($data);
}
}
}
return $saveTo;
}
I would start (if you haven't already) by getting a good IDE and a debugger as these will be invaluable tools. In this instance you may be able to use a profiler to work out where the memory is being used. Failing that some good ole' manual debugging code, say
$memory = memory_get_usage();
as the first line inside your inner loop. Then when you step through using the debugger you'll be able to see where the memory is ramping up.
btw, using global variables is generally not a good idea. You might want to pass in $threadColours as a parameter or look at other ways of getting that data into the function.

Crunch lots of files to generate stats file

I have a bunch of files I need to crunch and I'm worrying about scalability and speed.
The filename and filedata(only the first line) is stored into an array in RAM to create some statical files later in the script.
The files must remain files and can't be put into a databases.
The filename are formatted in the following fashion :
Y-M-D-title.ext (where Y is Year, M for Month and D for Day)
I'm actually using glob to list all the files and create my array :
Here is a sample of the code creating the array "for year" or "month" (It's used in a function with only one parameter -> $period)
[...]
function create_data_info($period=NULL){
$data = array();
$files = glob(ROOT_DIR.'/'.'*.ext');
$size = sizeOf($files);
$existing_title = array(); //Used so we can handle having the same titles two times at different date.
if (isSet($period)){
if ( "year" === $period ){
for ($i = 0; $i < $size; $i++) {
$info = extract_info($files[$i], $existing_file);
//Create the data array with all the data ordered by year/month/day
$data[(int)$info[5]][] = $info;
unset($info);
}
}elseif ( "month" === $period ){
for ($i = 0; $i < $size; $i++) {
$info = extract_info($files[$i], $existing_file);
$key = $info[5].$info[6];
//Create the data array with all the data ordered by year/month/day
$data[(int)$key][] = $info;
unset($info);
}
}
}
[...]
}
function extract_info($file, &$existing){
$full_path_file = $file;
$file = basename($file);
$info_file = explode("-", $file, 4);
$filetitle = explode(".", $info_file[3]);
$info[0] = $filetitle[0];
if (!isSet($existing[$info[0]]))
$existing[$info[0]] = -1;
$existing[$info[0]] += 1;
if ($existing[$info[0]] > 0)
//We have already found a post with this title
//the creation of the cache is based on info[4] data for the filename
//so we need to tune it
$info[0] = $info[0]."-".$existing[$info[0]];
$info[1] = $info_file[3];
$info[2] = $full_path_file;
$post_content = file(ROOT_DIR.'/'.$file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$info[3] = $post_content[0]; //first line of the files
unset($post_content);
$info[4] = filemtime(ROOT_DIR.'/'.$file);
$info[5] = $info_file[0]; //year
$info[6] = $info_file[1]; //month
$info[7] = $info_file[2]; //day
return $info;
}
So in my script I only call create_data_info(PERIOD) (PERIOD being "year", "month", etc..)
It returns an array filled with the info I need, and then I can loop throught it to create my statistics files.
This process is done everytime the PHP script is launched.
My question is : is this code optimal (certainly not) and what can I do to squeeze some juice from my code ?
I don't know how I can cache this (even if it's possible), as there is a lot of I/O involved.
I can change the tree structure if it could change things compared to a flat structure, but from what I found out with my tests it seems flat is the best.
I already thought about creating a little "booster" in C doing only the crunching, but I since it's I/O bound, I don't think it would make a huge difference and the application would be a lot less compatible for shared hosting users.
Thank you very much for your input, I hope I was clear enough here. Let me know if you need clarification (and forget my english mistakes).
To begin with you should use DirectoryIterator instead of glob function. When it comes to scandir vs opendir vs glob, glob is as slow as it gets.
Also, when you are dealing with a large amount of files you should try to do all your processing inside one loop, php function calls are rather slow.
I see you are using unset($info); yet in every loop you make, $info gets new value. Php does its own garbage collection, if thats your concern. Unset is a language construct not a function and should be pretty fast, but when using not needed, it still makes whole thing a bit slower.
You are passing $existing as a reference. Is there practical outcome for this? In my experience references make things slower.
And at last your script seems to deal with a lot of string processing. You might want to consider somekind of "serialize data and base64 encode/decode" solution, but you should benchmark that specifically, might be faster, might be slower depenging on your whole code. (My idea is that, serialize/unserialize MIGHT run faster as these are native php functions and custom functions with string processing are slower).
My answer was not very I/O related but I hope it was helpful.

Categories