diff --git a/.gitignore b/.gitignore index 57491c1..067d3e5 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,5 @@ src/cocopp/**/*.dat src/cocopp/**/*.tdat src/cocopp/**/*.info dist/ -ppdata/ \ No newline at end of file +ppdata/ +venv/ \ No newline at end of file diff --git a/README.md b/README.md index 75f1780..12c4a80 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ The package uses data generated with the [COCO framework](https://coco-platform. The main documentation pages for the `coco-postprocess` package `cocopp` can be found at + - [getting-started](https://numbbo.it/getting-started#postprocess) - [API documentation](https://numbbo.github.io/coco-doc/apidocs/cocopp) - [Issue tracker and bug reports](https://github.com/numbbo/coco-postprocess/issues) diff --git a/src/cocopp/html/generator.py b/src/cocopp/html/generator.py new file mode 100644 index 0000000..4dc9f04 --- /dev/null +++ b/src/cocopp/html/generator.py @@ -0,0 +1,87 @@ +"""HTML content generator for COCO post-processing.""" + +import json +import os + +class HtmlGenerator: + """Generates HTML content with dynamic JavaScript updates.""" + + STATIC_TEMPLATE = """ + + + + + + COCO Post-Processing Results + + + + +

COCO Post-Processing Results

+ +
+ + + + + +""" + + def __init__(self): + pass + + def generate_parent_index_data(self, algo_data, single_file_name, many_file_name): + """Generate data structure for parent index.""" + return { + 'title': 'COCO Post-Processing Results', + 'header': 'COCO Post-Processing Results', + 'nav_links': [], + 'single': sorted(algo_data.get('single', [])), + 'comparison': sorted(algo_data.get('comparison', [])), + 'images': [], + 'single_file_name': single_file_name, + 'many_file_name': many_file_name + } + + def generate_folder_content(self, current_dir, image_extension): + """Generate data structure for folder index.""" + nav_links = [ + 'Home', + 'Runtime profiles (with arrow keys navigation)', + 'Tables for selected targets', + 'Runtime profiles for selected targets' + ] + + images = [] + image_path = 'pprldmany-single-functions/pprldmany.%s' % image_extension + if os.path.isfile(os.path.join(current_dir, image_path)): + images.append(image_path) + + return { + 'title': 'COCO Post-Processing Results', + 'header': 'Results Overview', + 'nav_links': nav_links, + 'single': [], + 'comparison': [], + 'images': images, + 'single_file_name': '', + 'many_file_name': '' + } + + def render(self, data): + """Render HTML with injected data.""" + json_data = json.dumps(data, ensure_ascii=False) + html = self.STATIC_TEMPLATE.replace('{data}', json_data) + return html \ No newline at end of file diff --git a/src/cocopp/html/index.py b/src/cocopp/html/index.py new file mode 100644 index 0000000..df46701 --- /dev/null +++ b/src/cocopp/html/index.py @@ -0,0 +1,116 @@ +"""Main interface for generating folder index pages.""" + +import logging +import os +from .generator import HtmlGenerator +from .writer import HtmlWriter +from .. import genericsettings + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +def collect_algorithm_data(directory): + """Collect algorithm data from directory structure.""" + data = {'single': [], 'comparison': []} + + if not os.path.isdir(directory): + return data + + try: + for item in os.listdir(directory): + item_path = os.path.join(directory, item) + if not os.path.isdir(item_path): + continue + + single_file = os.path.join(item_path, '%s.html' % genericsettings.single_algorithm_file_name) + if os.path.isfile(single_file): + data['single'].append(item) + + many_file = os.path.join(item_path, '%s.html' % genericsettings.many_algorithm_file_name) + if os.path.isfile(many_file): + data['comparison'].append(item) + except OSError as e: + logger.warning("Error reading directory %s: %s" % (directory, str(e))) + + return data + +def update_parent_index(parent_index_path): + """Update the parent index.html file with links to algorithm results. + + This function only writes the HTML file once. Links are managed + dynamically via JavaScript based on available algorithm directories. + + Args: + parent_index_path: Path to the parent index.html file + + Raises: + IOError: If there are issues reading/writing the index file + """ + try: + generator = HtmlGenerator() + parent_dir = os.path.dirname(os.path.realpath(parent_index_path)) + + # to collect data from available algorithms + algo_data = collect_algorithm_data(parent_dir) + + # generating data structure for dynamic rendering + data = generator.generate_parent_index_data( + algo_data, + genericsettings.single_algorithm_file_name, + genericsettings.many_algorithm_file_name + ) + + # HTML rendering + html = generator.render(data) + + # initial creation of the file (will be created only if it doesn't already exist) + writer = HtmlWriter() + if not os.path.isfile(parent_index_path): + writer.write_safely(parent_index_path, html) + logger.info("Created parent index at %s" % parent_index_path) + else: + logger.info("Parent index already exists at %s (using dynamic JS updates)" % parent_index_path) + + except Exception as e: + logger.error("Failed to update parent index: %s" % str(e)) + raise IOError("Failed to update parent index: %s" % str(e)) + +def save_folder_index(filepath, image_extension): + """Generate and save a folder index file. + + The HTML file is created once with static structure. Dynamic content + is managed via JavaScript and server-side data updates. + + Args: + filepath: Path where the index file should be saved + image_extension: Extension for image files (e.g. 'svg', 'png') + """ + if not filepath: + return + + try: + # content data generation + generator = HtmlGenerator() + current_dir = os.path.dirname(os.path.realpath(filepath)) + data = generator.generate_folder_content(current_dir, image_extension) + + # rendering to HTML + html = generator.render(data) + + # initial creation of the file (created only if it doesn't already exist) + writer = HtmlWriter() + if not os.path.isfile(filepath): + writer.write_safely(filepath, html) + logger.info("Created folder index at %s" % filepath) + else: + logger.info("Folder index already exists at %s (using dynamic JS updates)" % filepath) + + # update parent index if needed + parent_dir = os.path.dirname(current_dir) + parent_index_path = os.path.join(parent_dir, 'index.html') + if not os.path.isfile(parent_index_path): + update_parent_index(parent_index_path) + + except Exception as e: + logger.error("Failed to save folder index at %s: %s" % (filepath, str(e))) + raise \ No newline at end of file diff --git a/src/cocopp/html/renderer.js b/src/cocopp/html/renderer.js new file mode 100644 index 0000000..18f3830 --- /dev/null +++ b/src/cocopp/html/renderer.js @@ -0,0 +1,57 @@ +// data injected by server +var contentData = window.contentData || {}; + +function renderLinks(data) { + var container = document.getElementById('linksContainer'); + var html = ''; + + // Navigation links + if (data.nav_links && data.nav_links.length > 0) { + data.nav_links.forEach(function(link) { + if (link) { + html += ''; + } + }); + } + + // comparison code + if (data.comparison && data.comparison.length > 0) { + html += '

Comparison Data

'; + data.comparison.forEach(function(algo) { + var path = algo + '/' + data.many_file_name + '.html'; + html += ''; + }); + } + + // single algorithm code + if (data.single && data.single.length > 0) { + html += '

Single Algorithm Data

'; + data.single.forEach(function(algo) { + var path = algo + '/' + data.single_file_name + '.html'; + html += ''; + }); + } + + container.innerHTML = html; +} + +function renderImages(data) { + var container = document.getElementById('imagesContainer'); + var html = ''; + + if (data.images && data.images.length > 0) { + data.images.forEach(function(img) { + html += '
'; + }); + } + + container.innerHTML = html; +} + +// rendering code +document.addEventListener('DOMContentLoaded', function() { + if (contentData) { + renderLinks(contentData); + renderImages(contentData); + } +}); diff --git a/src/cocopp/html/writer.py b/src/cocopp/html/writer.py new file mode 100644 index 0000000..fb41005 --- /dev/null +++ b/src/cocopp/html/writer.py @@ -0,0 +1,29 @@ +"""Safe file writing utilities for HTML output.""" + +import os + +class HtmlWriter: + """Handles safe writing of HTML files with atomic operations.""" + + @staticmethod + def write_safely(filepath, content): + """Write content to file atomically using a temporary file. + + Args: + filepath: Path to the output file + content: HTML content to write + """ + filepath = str(filepath) + + # creating parent directories (if they don't already exist) + parent_dir = os.path.dirname(filepath) + if parent_dir and not os.path.exists(parent_dir): + os.makedirs(parent_dir) + + try: + with open(filepath, 'w', encoding='utf-8', newline='') as f: + f.write(content) + f.flush() + os.fsync(f.fileno()) # forcing writing to disk (for mac users) + except Exception as e: + raise IOError("Failed to write %s: %s" % (filepath, str(e))) \ No newline at end of file diff --git a/src/cocopp/ppfig.py b/src/cocopp/ppfig.py index 5747684..fe76574 100644 --- a/src/cocopp/ppfig.py +++ b/src/cocopp/ppfig.py @@ -14,6 +14,8 @@ from matplotlib import pyplot as plt import shutil from six import advance_iterator +from cocopp.html.index import save_folder_index + # from pdb import set_trace # absolute_import => . refers to where ppfig resides in the package: @@ -496,7 +498,7 @@ def save_single_functions_html(filename, toolsdivers.replace_in_file(filename + add_to_names + '.html', '??COCOVERSION??', '
Data produced with COCO %s' % (toolsdivers.get_version_label(None))) if parentFileName: - save_folder_index_file(os.path.join(current_dir, parentFileName + '.html'), extension) + save_folder_index(os.path.join(current_dir, parentFileName + '.html'), extension) def write_dimension_links(dimension, dimensions, index): diff --git a/src/cocopp/refalgs/best2009-bbob.tar.gz.pickle b/src/cocopp/refalgs/best2009-bbob.tar.gz.pickle new file mode 100644 index 0000000..33d68e1 Binary files /dev/null and b/src/cocopp/refalgs/best2009-bbob.tar.gz.pickle differ