diff --git a/coastsnap/__init__.py b/coastsnap/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/coastsnap/__pycache__/utils.cpython-38.pyc b/coastsnap/__pycache__/utils.cpython-38.pyc new file mode 100644 index 0000000..ab2c253 Binary files /dev/null and b/coastsnap/__pycache__/utils.cpython-38.pyc differ diff --git a/coastsnap/fonts/Courier New Bold.ttf b/coastsnap/fonts/Courier New Bold.ttf new file mode 100644 index 0000000..894c455 Binary files /dev/null and b/coastsnap/fonts/Courier New Bold.ttf differ diff --git a/coastsnap/fonts/Courier New.ttf b/coastsnap/fonts/Courier New.ttf new file mode 100644 index 0000000..633899d Binary files /dev/null and b/coastsnap/fonts/Courier New.ttf differ diff --git a/coastsnap/fonts/Courier.ttf b/coastsnap/fonts/Courier.ttf new file mode 100644 index 0000000..97eb332 Binary files /dev/null and b/coastsnap/fonts/Courier.ttf differ diff --git a/coastsnap/fonts/Menlo.ttc b/coastsnap/fonts/Menlo.ttc new file mode 100644 index 0000000..128a79d Binary files /dev/null and b/coastsnap/fonts/Menlo.ttc differ diff --git a/coastsnap/fonts/SFNSText.ttf b/coastsnap/fonts/SFNSText.ttf new file mode 100644 index 0000000..36aa17f Binary files /dev/null and b/coastsnap/fonts/SFNSText.ttf differ diff --git a/coastsnap/photoshop_registration_all_sites.jsx b/coastsnap/photoshop_registration_all_sites.jsx index 69b26e0..26a1bcc 100644 --- a/coastsnap/photoshop_registration_all_sites.jsx +++ b/coastsnap/photoshop_registration_all_sites.jsx @@ -1,21 +1,21 @@ -// TO DO - -// Create another script that can run +// SCRIPT LOGIC // for site in sites: // for year in years: // register_images() + // VARIABLE DEFINITIONS +// images_to_register = All images to be registered for a given site and year (no target or seed) +// batch_images_to_register = Batch images to be registered (no target or seed) +// batch_images_all = Target, seed and batch images to be registered -// images_to_register = All images to be registered for a given site and year (no target or seed) -// batch_images_to_register = Batch images to be registered (no target or seed) -// batch_images_all = Target, seed and batch images to be registered -var batch_size = 10; +var batch_size = 15; // Must use forwardslashes in filepath, not backslashes var batch_download_csv = File("C:/Users/z5079346/OneDrive - UNSW/Projects/Coastsnap_test/CoastSnap_Sites.csv") + // retreive site names from batch_download.csv var csv_data=[]; batch_download_csv.open('r'); @@ -26,8 +26,8 @@ while(!batch_download_csv.eof){ batch_download_csv.close(); var site_names = csv_data.toString().split(",") -// Images parent directory -var parent_folder_path = "C:/Users/z5079346/OneDrive - UNSW/Projects/Coastsnap_test/Images_Test"; +// Retrieve images parent directory from CoastSnap_Sites.csv +var parent_folder_path = File(site_names[9]); var batch_images_to_register = []; // Used in exportLayersToPNG @@ -88,6 +88,7 @@ function batch_register_images(batchIndex, site_path, site_name, images_to_regis } stackFiles(batch_images_all); + resizeLayers(); lockTarget(); selectAllLayers(); autoAlign(); @@ -126,47 +127,74 @@ function imagesNotRegistered(site_images_all, photoshop_images) { function stackFiles(sFiles){ var loadLayersFromScript = true; - var SCRIPTS_FOLDER = decodeURI(app.path + '/' + localize('$$$/ScriptingSupport/InstalledScripts=Presets/Scripts')); - $.evalFile( new File(SCRIPTS_FOLDER + '/Load Files into Stack.jsx')); - loadLayers.intoStack(sFiles, false); }; +function resizeLayers() { + + var doc = app.activeDocument; + var targetImage = app.activeDocument.artLayers.getByName("Target.jpg") + + // Retrieve Target image dimensions + var target_bounds = targetImage.bounds; + var target_width = target_bounds[2] - target_bounds[0]; + var target_height = target_bounds[3] - target_bounds[1]; + + // Update Target Image dimensions + var new_width = 1280; + preferences.rulerUnits = Units.PIXELS; + var s = (new_width/target_width)*100; + targetImage.resize(s, s, AnchorPosition.TOPLEFT) + + // Retrieve new Target image dimensions + var target_bounds = targetImage.bounds; + var target_width = target_bounds[2] - target_bounds[0]; + var target_height = target_bounds[3] - target_bounds[1]; + + // Make layers similar dimensions to new Target + var layerName = doc.layers; + for (var ii = 0; ii < layerName.length; ii++) { //looping through all the layers + var current_layer = layerName[ii]; + preferences.rulerUnits = Units.PIXELS; + var b = current_layer.bounds; + var layer_width = b[2]-b[0]; + var layer_height = b[3]-b[1]; + if (layer_height >= layer_width) { + var s = (target_height/layer_height)*100; + } + else { + var s = (target_width/layer_width)*100; + } + current_layer.resize(s, s, AnchorPosition.TOPLEFT) + } +} + function selectAllLayers() { var desc = new ActionDescriptor(); - var ref = new ActionReference(); - ref.putEnumerated( charIDToTypeID('Lyr '), charIDToTypeID('Ordn'), charIDToTypeID('Trgt') ); - desc.putReference( charIDToTypeID('null'), ref ); - executeAction( stringIDToTypeID('selectAllLayers'), desc, DialogModes.NO ); }; + function autoAlign() { var desc = new ActionDescriptor(); - var ref = new ActionReference(); - + ref.putEnumerated( charIDToTypeID('Lyr '), charIDToTypeID('Ordn'), charIDToTypeID('Trgt') ); - desc.putReference( charIDToTypeID('null'), ref ); - desc.putEnumerated( charIDToTypeID('Usng'), charIDToTypeID('ADSt'), stringIDToTypeID('ADSContent') ); - desc.putEnumerated( charIDToTypeID('Aply'), stringIDToTypeID('projection'), charIDToTypeID('Auto') ); - desc.putBoolean( stringIDToTypeID('vignette'), false ); - desc.putBoolean( stringIDToTypeID('radialDistort'), false ); - + executeAction( charIDToTypeID('Algn'), desc, DialogModes.NO ); }; @@ -292,7 +320,6 @@ function exportLayersToPNG(target_width, target_height, batch_images_to_register SavePNG(saveFile); app.activeDocument.close(SaveOptions.DONOTSAVECHANGES); } - }; function dupLayers() { @@ -309,10 +336,10 @@ function dupLayers() { function SavePNG(saveFile){ var pngOpts = new ExportOptionsSaveForWeb; - pngOpts.format = SaveDocumentType.PNG + pngOpts.format = SaveDocumentType.JPEG; pngOpts.PNG8 = false; pngOpts.transparency = true; pngOpts.interlaced = false; - pngOpts.quality = 200; + pngOpts.quality = 100; activeDocument.exportDocument(new File(saveFile),ExportType.SAVEFORWEB,pngOpts); } \ No newline at end of file diff --git a/coastsnap/tag_registered.py b/coastsnap/tag_registered.py index c44224c..80267cf 100644 --- a/coastsnap/tag_registered.py +++ b/coastsnap/tag_registered.py @@ -12,11 +12,11 @@ import attr import cv2 import numpy as np import pandas as pd -import typer from loguru import logger from moviepy.editor import * -from utils import divide_chunks, datenum_to_datetime, nearest +from utils import divide_chunks, datenum_to_datetime, nearest, progressbar, RegisteredImage +import time # progress bar from PIL import Image, ImageFont, ImageDraw import os @@ -27,31 +27,145 @@ import openpyxl import scipy.io as sio from datetime import datetime, timedelta -# Enables command-line interface -app = typer.Typer() - #-----------------------------------------------------------------------------# # Update this file path for Image Tagging - Tide Data # Example: parent_dir = '/Users/admin/OneDrive - UNSW/My files/CoastSnap/' -parent_dir = r"C:\Users\z5079346\OneDrive - UNSW\My files\CoastSnap" +parent_dir_ACTUAL = r"C:\Users\z5079346\OneDrive - UNSW\My files\CoastSnap" #-----------------------------------------------------------------------------# +# RETRIEVE IMAGES PARENT DIRECTORY IN .csv# + +coastsnap_sites = pd.read_csv("C:/Users/z5079346/OneDrive - UNSW/Projects/Coastsnap_test/CoastSnap_Sites.csv") +parent_directory = coastsnap_sites.parent_directory[0] +print(parent_directory) + +#-----------------------------------------------------------------------------# + + +folder = r"C:\Users\z5079346\OneDrive - UNSW\Projects\Coastsnap_test\Images_Test\cathieillaroo\Photoshop\2021" + +''' +For each site in Images Parent Directory: + Iterate through years in reverse: + Iterate through images names (dates) in reverse: + Check if image name in 'Registered'? If not, tag and save + For this step, do image_name.replace('Processed' with 'Registered') + + + +''' +def get_site_tide_data(parent_dir, site): + + # Retrieve tide data for the given site + db = openpyxl.load_workbook(parent_dir + "/Database/CoastSnapDB.xlsx") + beach_data = db[site] + tide_filename = beach_data["B24"].value + + if tide_filename == 'NO_TIDE.mat': + return False + + mat_path = parent_dir + '/Tide Data/' + tide_filename + mat = sio.loadmat(mat_path) + tide_dict = mat['tide'] + ts = tide_dict[0][0] # Format of tide/time Matlab data is gross + tt = list(ts[0]) # ts[0] = tides, ts[1] = times, ts[2] = + + print(site + ": " +"Loading tide data... (this may take half a minute)") + tide_times = [datenum_to_datetime(i) for i in tt] # THIS STEP TAKES A LONG TIME + + return tide_times, ts + + +def tag_image(image2tag): + + image = Image.open(image2tag.pathname) + draw = ImageDraw.Draw(image) + image2tag.get_dimensions() + + # White Text Box + rect_height = image2tag.height/20 + + # Create white text box + draw.rectangle((0, 0, image2tag.width, rect_height), fill='white') + + # Tag image with text + draw.text((20, rect_height/4),image2tag.tag, font = image2tag.font, fill=(0, 0, 0)) + + registered_path_wrong = image2tag.pathname[:-4] + '_registered.jpg' + registered_path = registered_path_wrong.replace('Photoshop', 'Registered') + image = image.convert('RGB') + + image.save(registered_path) + +for site in os.listdir(parent_directory): # Loop through SITES + i=0 + site_complete = False # A flag to stop tagging images when found + tide_data = False + font = None + photoshop_path = parent_directory +'/'+ site + '/Photoshop' + try: # Check if site contains 'Processed' directory + years_list = os.listdir(photoshop_path) + years_list.reverse() + except: + continue + for year in years_list: # Loop through YEARS + if site_complete: break + year_path = photoshop_path + '/' + year + image_list = os.listdir(year_path) + image_list.reverse() + for image_filename in image_list: # Loop through IMAGES + #print(image_filename) + registered_year_path = year_path.replace('Photoshop', 'Registered') + registered_image_path = registered_year_path + '/' + image_filename[:-4] + '_registered.jpg' + if site not in image_filename: + continue + # Check if image already tagged + if os.path.isfile(registered_image_path): + site_complete = True; + if i == 0: + print(site + ": " + str(i) + " images tagged") + elif image2tag.tide: + print(site + ": " + str(i) + " images tagged with tide") + else: + print(site + ": " + str(i) + " images tagged no tide") + break + else: + if not os.path.exists(registered_year_path): # Chech that registered/year directory exists + os.makedirs(registered_year_path) # if not, create it + i += 1 + pathname = os.path.join(year_path, image_filename) + image2tag = RegisteredImage(pathname, image_filename) # Create image object + if i == 1: # Retrieve Tide Data once for each site + site_tide_data, ts = get_site_tide_data(parent_dir_ACTUAL, site) + + font = image2tag.get_font() + + if site_tide_data: + image2tag.get_tide(site_tide_data, ts) + + image2tag.create_tag() + tag_image(image2tag) + + + -@app.command() -def tagging( - folder: Path = typer.Argument(None, help="Folder with images"), - photoshop: bool = typer.Option(True, help="Have these images been registered with photoshop?"), - tide: bool = typer.Option(False, help="Do you want to add the tide to the image tag?") -): - """ - Tags images based on file name. - - Requires font file in coastsnap/fonts directory - """ - from PIL import Image, ImageFont, ImageDraw # Throws errors if this isn't here.. + + + +""" +Tags images based on file name. +- Requires font file in coastsnap/fonts directory +""" + +""" +def tag_images(folder): + photoshop = True + tide = False + # Get image paths img_paths = [x for x in Path(folder).glob("*.jpg")] img_names = [str(x) for x in img_paths] @@ -59,8 +173,8 @@ def tagging( # Initialise white text box rect_height = 1 - width = 1 - + image_width = 1 + # Check whether the directory 'tagged' exists or not path_name = img_names[0] tagged_dir = str(Path(path_name).parent) + '/tagged' @@ -68,55 +182,29 @@ def tagging( if not isExist: # Create a new directory because it does not exist os.makedirs(tagged_dir) - - - - # # Set Image Label Info Based on Target Image - # target_image_path = str(Path(path_name).parent.parent.parent) + r"\Target Image\Target.JPG" - # print(target_image_path) - # image = Image.open(target_image_path) - # img_fraction = 0.6 # Change this to change the font size - # fontsize = 1 - # # White Text Box - # width, height = image.size - # rect_height = height/20 - # rect_width = width/1.5 - # font = ImageFont.truetype("fonts/Courier New Bold.ttf", fontsize) - - # while font.getsize(txt)[0] < img_fraction*image.size[0]: - # # iterate until the text size is just larger than the criteria - # fontsize += 1 - # font = ImageFont.truetype("fonts/Courier New Bold.ttf", fontsize) - - #"C:\Users\z5079346\OneDrive - UNSW\My files\CoastSnap\Images\cathieillaroo\Registered\2022" - #"C:\Users\z5079346\OneDrive - UNSW\My files\CoastSnap\Images\cathieillaroo\Target Image\Target.JPG" - - - # Tide Data if tide: # Retrieve the site name from the first image filename = Path(img_names[0]).name - if photoshop == True: - fname = filename[6:-4] - else: - fname = filename - filename_list = fname.split(".") + + filename_list = filename.split(".") site_name = filename_list[6] # Retrieve tide data for the given site - db = openpyxl.load_workbook(parent_dir + "Database/CoastSnapDB.xlsx") + db = openpyxl.load_workbook(parent_dir + "/Database/CoastSnapDB.xlsx") beach_data = db[site_name] tide_filename = beach_data["B24"].value - mat_path = parent_dir + 'Tide Data/' + tide_filename - print("Loading tide data... (this may take half a minute)") + mat_path = parent_dir + '/Tide Data/' + tide_filename mat = sio.loadmat(mat_path) tide_dict = mat['tide'] ts = tide_dict[0][0] # Format of tide/time Matlab data is gross tt = list(ts[0]) # ts[0] = tides, ts[1] = times, ts[2] = - tide_times = [datenum_to_datetime(i) for i in tt] - + + print("Loading tide data... (this may take half a minute)") + tide_times = [datenum_to_datetime(i) for i in tt] # THIS STEP TAKES A LONG TIME + + fontsize = 1 for index, img in enumerate(sorted(img_names)): print("Image " + str(index+1)) @@ -126,12 +214,7 @@ def tagging( # Retrieve tag information from file name - if photoshop == True: - fname = filename[6:-4] - else: - fname = filename - - filename_list = fname.split(".") + filename_list = filename.split(".") posix_time = filename_list[0] date = filename_list[3].split("_") hour = date[1] @@ -148,7 +231,7 @@ def tagging( # Retrieve tide data if tide: - + # Account for daylight savings # ASSUMPTION: All .mat tide files are in AEST. if timezone == 'AEDT': @@ -174,18 +257,18 @@ def tagging( txt = ('Date:' + year + '/' + month + '/' + day + ' Time:' + hour + ':' + minute + ' Contributor:' + contributor) - + # Set the fontsize, such that the tag covers 50% the width of the first image if index == 0: - img_fraction = 0.6 # Change this to change the font size + img_fraction = 0.8 # Change this to change the font size # White Text Box - width, height = image.size - rect_height = height/20 - rect_width = width/1.5 + image_width, image_height = image.size + rect_height = image_height/20 + rect_width = image_width/1.5 font = ImageFont.truetype("fonts/Courier New Bold.ttf", fontsize) - + while font.getsize(txt)[0] < img_fraction*image.size[0]: # iterate until the text size is just larger than the criteria fontsize += 1 @@ -193,18 +276,43 @@ def tagging( font = ImageFont.truetype("fonts/Courier New Bold.ttf", fontsize) # Create white text box - draw.rectangle((0, 0, width, rect_height), fill='white') - + draw.rectangle((0, 0, image_width, rect_height), fill='white') + # Tag image with text draw.text((20, rect_height/4),txt, font = font, fill=(0, 0, 0)) - new_name = fname[:-4] + '_registered.jpg' + new_name = filename + '_registered.jpg' print(new_name + '\n') new_path = tagged_dir + "/" + new_name + image = image.convert('RGB') image.save(new_path) - logger.info(f"Tagged Images Saved") + logger.info(f"Tagged Images for " + + " Saved") + + +tag_images(folder) + +""" + + +# # Set Image Label Info Based on Target Image (PUT AT LINE 90) +# target_image_path = str(Path(path_name).parent.parent.parent) + r"\Target Image\Target.JPG" +# print(target_image_path) +# image = Image.open(target_image_path) +# img_fraction = 0.6 # Change this to change the font size +# fontsize = 1 + +# # White Text Box +# width, height = image.size +# rect_height = height/20 +# rect_width = width/1.5 +# font = ImageFont.truetype("fonts/Courier New Bold.ttf", fontsize) + +# while font.getsize(txt)[0] < img_fraction*image.size[0]: +# # iterate until the text size is just larger than the criteria +# fontsize += 1 +# font = ImageFont.truetype("fonts/Courier New Bold.ttf", fontsize) -if __name__ == "__main__": - app() +#"C:\Users\z5079346\OneDrive - UNSW\My files\CoastSnap\Images\cathieillaroo\Registered\2022" +#"C:\Users\z5079346\OneDrive - UNSW\My files\CoastSnap\Images\cathieillaroo\Target Image\Target.JPG" diff --git a/coastsnap/utils.py b/coastsnap/utils.py new file mode 100644 index 0000000..da1fdd6 --- /dev/null +++ b/coastsnap/utils.py @@ -0,0 +1,138 @@ +import numpy as np +from datetime import datetime, timedelta +import sys +from time import strptime +from PIL import Image, ImageFont + +def divide_chunks(l, n): + """ + Splits a list into chunks of length n. Used to process the images in chunks. + """ + for i in range(0, len(l), n): + yield l[i : i + n] + +# Sourced from https://gist.github.com/victorkristof/b9d794fe1ed12e708b9d +def datenum_to_datetime(datenum): + """ + Convert Matlab datenum into Python datetime. + :param datenum: Date in datenum format + :return: Datetime object corresponding to datenum. + """ + days = datenum % 1 + hours = days % 1 * 24 + minutes = hours % 1 * 60 + seconds = np.round(minutes % 1 * 60) + return datetime.fromordinal(int(datenum)) \ + + timedelta(days=int(days)) \ + + timedelta(hours=int(hours)) \ + + timedelta(minutes=int(minutes)) \ + + timedelta(seconds=int(seconds)) \ + - timedelta(days=366) + + +# Sourced from https://stackoverflow.com/questions/32237862/find-the-closest-date-to-a-given-date +def nearest(items, pivot): + """ + This function will return the datetime in items + which is the closest to the date pivot + """ + return min(items, key=lambda x: abs(x - pivot)) + + +def progressbar(it, prefix="", size=60, out=sys.stdout): # Python3.3+ + count = len(it) + def show(j): + x = int(size*j/count) + print("{}[{}{}] {}/{}".format(prefix, u"#"*x, "."*(size-x), j, count), + end='\r', file=out, flush=True) + show(0) + for i, item in enumerate(it): + yield item + show(i+1) + print("\n", flush=True, file=out) + + +class RegisteredImage(): + + def __init__(self, pathname, filename): + self.pathname = pathname + self.filename = filename + filename_list = filename.split(".") + date = filename_list[3].split("_") + if 'snap' in filename_list: + self.contributor = filename_list[8] # Mitch filename format + else: + self.contributor = filename_list[6] # Leaman filename format + self.site = filename_list[6] + self.posix_time = filename_list[0] + self.timezone = filename_list[4] + self.year = filename_list[5] + self.month = '{:02d}'.format(strptime(filename_list[2],'%b').tm_mon) # Ensure 2-digit format + self.day = date[0] + self.hour = date[1] + self.minute = date[2] + self.second = date[3] + + self.tide = None + self.tag = None + self.width = None + self.height = None + self.font = None + + def get_tide(self, site_tide_data, ts): + + # Account for daylight savings + # ASSUMPTION: All .mat tide files are either AEST or AEDT + if self.timezone == 'AEDT': + self.hour = str(int(self.hour) - 1) + date_string = self.year + '-' + self.month + '-' + self.day + ' ' + self.hour + ':' + self.minute + ':' + self.second + img_datetime = datetime.strptime(date_string, "%Y-%m-%d %H:%M:%S") # Image date/time as a datetime object + + tide_date = nearest(site_tide_data, img_datetime) + mat_index = site_tide_data.index(tide_date) # Retrieve the index of the .mat tide/time + mat_tide = round(ts[1][mat_index][0], 2) # Associated tide + + #print('Image date/time: ' + date_string) + #print('Tide record: ' + str(tide_date)) + self.tide = "{:.2f}".format(mat_tide) + + def create_tag(self): + # Create image tag + hour = self.hour.zfill(2) + minute = self.minute.zfill(2) + + if self.tide: + tide = self.tide + if float(self.tide) >= 0: + tide = '+' + self.tide + self.tag = ('Date:' + self.year + '/' + self.month + '/' + self.day + + ' Time:' + hour + ':' + minute + + ' Tide:' + tide + 'm AHD' + + ' Contributor:' + self.contributor) + else: + self.tag = ('Date:' + self.year + '/' + self.month + '/' + self.day + + ' Time:' + hour + ':' + minute + + ' Contributor:' + self.contributor) + + def get_dimensions(self): + image = Image.open(self.pathname) + # White Text Box + self.width, self.height = image.size + + + def get_font(self): + + image = Image.open(self.pathname) + fontsize = 1 + if self.tide: + img_fraction = 0.95 + else: + img_fraction = 0.85 + font = ImageFont.truetype("fonts/Courier New Bold.ttf", fontsize) + generic_large_tag = "1641775682.Mon.Jan.10_11_48_02.AEDT.2022.byron.snap.KateThornborough123" + + while font.getsize(generic_large_tag)[0] < img_fraction*image.size[0]: + # iterate until the text size is just larger than the criteria + fontsize += 1 + font = ImageFont.truetype("fonts/Courier New Bold.ttf", fontsize) + self.font = ImageFont.truetype("fonts/Courier New Bold.ttf", fontsize) \ No newline at end of file diff --git a/environment.yml b/environment.yml index 88db386..28644c7 100644 --- a/environment.yml +++ b/environment.yml @@ -8,6 +8,7 @@ dependencies: - opencv - numpy - loguru + - typer - attrs - pandas - black diff --git a/registration_capacity.csv b/registration_capacity.csv new file mode 100644 index 0000000..69eacf1 --- /dev/null +++ b/registration_capacity.csv @@ -0,0 +1,38 @@ +site_name,root_id,limit,type,Registration Capacity,Comment,parent_directory +alex,487451,300,CoastSnap Station,Good,Hand rail in foreground,C:\Users\z5079346\OneDrive - UNSW\My files\CoastSnap\Images +birubi,548070,300,CoastSnap Station,Good,Structures in foreground, +blacksmiths,269988,300,CoastSnap Station,Good,, +broulee,269990,300,CoastSnap Station,Good,Bad images small and tagged, +buddina,487447,300,CoastSnap Station,Ok,, +burleigh,303934,300,CoastSnap Station,Good,Distant view. Building., +byron,269991,300,CoastSnap Station,Ok,Distant view. Zoomed images turn small, +cathieillaroo,393016,300,CoastSnap Station,,, +cathielagoon,392988,300,CoastSnap Station,,, +coogee,286418,300,DIY,,, +coolum,487449,300,CoastSnap Station,,, +cooya,275690,300,CoastSnap Station,Very Bad,,Bad Seed Images +cowbay,275692,300,CoastSnap Station,,, +era,297634,300,CoastSnap Station,,, +fourmile,275689,300,CoastSnap Station,,, +frankston,425039,300,CoastSnap Station,,, +garie,296239,300,CoastSnap Station,,, +hungry,296903,300,CoastSnap Station,,, +kirra,270011,300,CoastSnap Station,,, +macsnth,257393,300,DIY,,, +macssth,404754,300,DIY,,, +manly,242186,300,CoastSnap Station,,, +moffat,487448,300,CoastSnap Station,,, +newell,275691,300,CoastSnap Station,,, +nthnarra,243537,300,CoastSnap Station,,, +queenscliff,269334,300,CoastSnap Station,,, +rainbow,451612,300,CoastSnap Station,,, +seaford,421320,300,CoastSnap Station,,, +shortpoint,269992,300,CoastSnap Station,,, +stockton1,269985,300,CoastSnap Station,,, +stockton2,269986,300,CoastSnap Station,,, +stockton3,269987,300,CoastSnap Station,,, +tomakin,269989,300,CoastSnap Station,,, +tugun,269993,300,CoastSnap Station,,, +wamberal,299431,300,CoastSnap Station,,, +wonga,268307,300,CoastSnap Station,,, +woolgooga,435190,300,CoastSnap Station,,, diff --git a/workflow.pptx b/workflow.pptx new file mode 100644 index 0000000..0c8ffce Binary files /dev/null and b/workflow.pptx differ