HTMLLogRenderer (with screenshot)

A place for users of OGRE to discuss ideas and experiences of utilitising OGRE in their games / demos / applications.
Post Reply
User avatar
johnhpus
Platinum Sponsor
Platinum Sponsor
Posts: 1186
Joined: Sat Apr 17, 2004 2:49 am
x 3

HTMLLogRenderer (with screenshot)

Post by johnhpus »

I've put together a class that derives from LogListener and shadows a given Log's output with an html version that adds a couple of features.

You can specify an image to appear at the top of the html file and specify it's dimensions.

You can give HTMLLogRenderer a list of strings to be displayed with newlines between them as a header to the file.

You can specify special formatting strings to be applied to Log entries depeding on the first word in the entry. For instance if you've defined a style whose "flag" is "Error:" with text color white and highlight color red, that formatting will be applied to each Log message that starts with "Error:". A style can have user prefix and postfix strings which can be used for things like making the message a url or adding an annoying <blink>.

You can log "Additional Information" by logging a message in this format: +Section_Name Entry_String. The information is displayed at the bottom of the file with each Entry_String appearing numbered below each Section_Name.

Here's what it looks like in practice. This is a contrived example because Ogre's various log entries obviously don't take this naming scheme into account.

Image

Here's the config file that defines the styles used in that screenshot. You'll need to either copy the ogrelogo-small.jpg image to the program's running folder or change the path to the image.

Code: Select all

# comment out these top two lines if you don't want an image at the top
headerImage=ogrelogo-small.jpg
headerImageDimensions=269 158
# style=FLAG TEXT_COLOR HIGHLIGHT_COLOR
style=App: blue white
style=Error: red white
style=Fatal_Error: white red
style=Resources: green white
style=Info: orange white
style=Blinky: black white <blink> </blink> 
And here's the actual class:

Code: Select all

#ifndef __HTMLLogRenderer_h__
#define __HTMLLogRenderer_h__

#include "Ogre.h"
using namespace Ogre;

#include <list>
#include <map>
using namespace std;

typedef list<String> StringList;
typedef map<String,StringList*> StringMap;

class StringUtilEx : public StringUtil
{
public:
	/* 
	found this find and replace code on the internet at
	http://www.pscode.com/vb/scripts/ShowCode.asp?txtCodeId=7447&lngWId=3
	I don't know if it's ok to just take it or not 
	*/
	static string findAndReplace( String str, String find, String replaceWith )
	{
		unsigned long iIndex1 = str.find(find, 0);
		unsigned long iIndex2 = 0;
		unsigned long iLengthOld = find.length();
		unsigned long iLengthNew = replaceWith.length();
		while (iIndex1 != string::npos)
		{
			iIndex2 = iIndex1 + iLengthNew + 1;
			str = str.erase(iIndex1, iLengthOld);
			str = str.insert(iIndex1, replaceWith);
			iIndex1 = str.find(find, iIndex2);
		}

		return str;
	}

	static bool areStringsEqual( String str1, String str2 )
	{
		int length = ( (str1.length() > str2.length()) ? str1.length() : str2.length() );
		if( !strncmp( str1.c_str(), str2.c_str(), length ) )
			return true;

		return false;
	}
};

class HTMLLogRenderer : public LogListener
{
protected:
	// stores the formatting strings that are put before and after a message if
	// it's first word matches the style's flag string
	class Style
	{
	public:
		Style( String flag, String prefix, String postfix )
		{
			mFlag = flag;
			mPrefix = prefix;
			mPostfix = postfix;
		}
		String mFlag;
		String mPrefix;
		String mPostfix;
	};
	typedef list<Style*> StyleList;

public:
	HTMLLogRenderer( const String &originalLogFilename, const String &rendererConfigFilename,
		StringList &headerInfo ) : LogListener()
	{
		mLoggingLevel = LL_NORMAL;
		mOriginalLogFilename = originalLogFilename;
		mNumEntries = 0;

		// open renderer config file
		ConfigFile cf;
		cf.loadDirect( rendererConfigFilename, "\t=" );
		
		String headerImageFilename;
		int headerImageWidth = 0;
		int headerImageHeight = 0;

		// parse the config file
		ConfigFile::SettingsIterator config_iter = cf.getSettingsIterator();
		while( config_iter.hasMoreElements() )
		{
			String key = config_iter.peekNextKey();
			String value = config_iter.peekNextValue();
            
			// if this element is a style, add it
			if( StringUtilEx::areStringsEqual( key, "style" ) )
			{
				stringstream ss( value );
				String flag, textColour, highlightColour, prefix, postfix;

                ss >> flag >> textColour >> highlightColour >> prefix >> postfix;
				addStyle( flag, textColour, highlightColour, prefix, postfix );
			}
			// set the header image filename
			else if( StringUtilEx::areStringsEqual( key, "headerImage" ) )
			{
				stringstream ss( value );
				ss >> headerImageFilename;
			}
			// set the header dimensions
			else if( StringUtilEx::areStringsEqual( key, "headerImageDimensions" ) )
			{
				stringstream ss( value );
				ss >> headerImageWidth >> headerImageHeight;
			}

			config_iter.getNext();
		}

		// compile renderer output filename
		String newFilename = originalLogFilename;
		newFilename.append( ".html" );
		mFile.open( newFilename.c_str() );

        // write HTML header
		mFile << "<header></header><body>";
		if( !StringUtilEx::areStringsEqual( "", headerImageFilename ) )
		{
			mFile << "<img src=\"" << headerImageFilename << "\" ";
			//if header image dimensions have been specified ( and the value isn't 0 ) write dimensions		
			if( headerImageHeight > 0 && headerImageWidth > 0 )
				mFile << " width=" << headerImageWidth << " height=" << headerImageHeight;
			// close img tag 
			mFile << "><br>";
		}

		// write font tag
		mFile << "<font style=\"FONT-FAMILY: \'Courier New\'\" size=2>";

		// write any supplied header strings
		for( StringList::iterator iter = headerInfo.begin(); iter != headerInfo.end(); ++iter )
		{
			String headerLine = static_cast<String>(*iter);
			mFile << headerLine << "<br>";
		}

		// write the stuff that preceeds the log entries
		mFile << "<br>Log<br>";
		mFile << "--------------------------------------------------" << "<br>";
		mFile << "</b>";

		mFile.flush();
	}

	virtual ~HTMLLogRenderer()
	{
		// this is the line that appears after the last message in the log
		mFile << "--------------------------------------------------" << "<br>";
		mFile << "</b>";
		mFile << "<br>";
		renderAdditionalInfo();
		mFile << "</body>";

		// not sure if it's necessary to call this before close()
		mFile.flush();
		mFile.close();

		// delete the styles
		for( StyleList::iterator iter = mStyleList.begin(); iter != mStyleList.end(); iter )
		{
			Style *style = *iter;
			delete style;
			iter = mStyleList.erase( iter );
		}
	}

	virtual void write( const String& name, const String& message, 
		LogMessageLevel lml = LML_NORMAL, bool maskDebug = false )
	{
		// check if the message is meant for the log this is shadowing
		if( StringUtilEx::areStringsEqual( name, mOriginalLogFilename ) )
			// check for the '+' sign that indicates an additional info entry
			if( StringUtil::startsWith( message, "+" ) )
				addAdditionalInfo( message );
			// check if the logging level of the message meets the threshold, copied from the Log class
			else if( ( mLoggingLevel + lml ) >= OGRE_LOG_THRESHOLD )
			{
				// write time ( slightly changed code from Ogre's log class )
				struct tm *pTime;
				time_t ctTime; time(&ctTime);
				pTime = localtime( &ctTime );
				mFile << "#" << std::setw(4) << std::setfill('0') << mNumEntries++;
				mFile
					<< " " << std::setw(2) << std::setfill('0') << pTime->tm_hour
					<< ":" << std::setw(2) << std::setfill('0') << pTime->tm_min
					<< ":" << std::setw(2) << std::setfill('0') << pTime->tm_sec;
				mFile << "&nbsp;";

				// get the first word in the message, which will be the prefix if one is present
				stringstream ss( message );
				String firstWord;
				ss >> firstWord;

				// if firstWord is equal to a style's flag string, get a pointer to that style
				Style *style = 0;
				for( StyleList::iterator iter = mStyleList.begin(); iter != mStyleList.end(); ++iter )
				{				
					if( StringUtilEx::areStringsEqual( firstWord, static_cast<Style*>(*iter)->mFlag ) )
						style = *iter;
				}

				// this string is the string to be output to mFile
				String outMessage;
				// if a style was found
				if( style )
				{
					// add the prefix and postfix around the original message
					outMessage.append( style->mPrefix );
					outMessage.append( message );
					outMessage.append( style->mPostfix );
				}
				// otherwise just copy the original message
				else
				{
					outMessage = message;
				}
			
				// change any \n newlines into HTML's <br>, 
				// and add enough spaces so that the new line is indented past the entry number and time
				outMessage = StringUtilEx::findAndReplace( outMessage, "\n", "<br>\t\t\t\t&nbsp;&nbsp;&nbsp;" );
				// change any \t tabs into five HTML &nbsp
				outMessage = StringUtilEx::findAndReplace( outMessage, "\t", "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;" );
				
				outMessage.append( "<br>" );

				mFile << outMessage;
				mFile.flush();
			}
	}

	void setLoggingLevel( LoggingLevel lml )
	{
		mLoggingLevel = lml;
	}

protected:
	void addStyle( String flag, 
		String textColour, String highlightColour, 
		String customPrefix, String customPostfix )
	{
		stringstream prefix;
		prefix << "<font  color=\""	<< textColour << 
			"\" style=\"FONT-FAMILY: 'Courier New';BACKGROUND-COLOR:" << highlightColour << "\" size=2>" << customPrefix;
		stringstream postfix;
		postfix << customPostfix << "</font>";

		Style *style = new Style( flag, prefix.str(), postfix.str() );
		mStyleList.push_back( style );
	}

	void addAdditionalInfo( String sInfo )
	{
		stringstream ss( sInfo );
		String section;
		String message;

		ss >> section;
		message = sInfo.substr( section.length(), sInfo.length() );
		
		// remove '+' character
		section = section.substr( 1, section.length() - 1 );
		
		StringList *itemList = 0;
		for( StringMap::iterator iter = mAdditionalInfo.begin(); iter != mAdditionalInfo.end(); ++iter )
		{
			if( !strncmp( static_cast<String>(iter->first).c_str(), section.c_str(), section.length() ) )
				itemList = iter->second;
		}

		if( !itemList )
		{
			StringList *itemList = new StringList;
			mAdditionalInfo[section] = itemList;
		}

		itemList = mAdditionalInfo[section];
		itemList->push_back( message );
	}

	void renderAdditionalInfo()
	{
		mFile << "Additional Info: " << "<br>";
		mFile << "--------------------------------------------------" << "<br><br>";
		// for each additional info section
		for( StringMap::iterator k_iter = mAdditionalInfo.begin(); k_iter != mAdditionalInfo.end(); ++k_iter )
		{
			string headerName = k_iter->first;
			// underscores can be substituted for space in a section name, so fix that up
			headerName = StringUtilEx::findAndReplace( headerName, "_", "&nbsp;" );
			mFile << headerName << ": " << "<br>";

			int i = 1;
			StringList *lines = k_iter->second;
			// for each entry in the section
			for( StringList::iterator v_iter = lines->begin(); v_iter != lines->end(); ++v_iter )
			{
				String line = *v_iter;
				mFile << "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;" << i << ")" << "&nbsp;" << line << "<br>";
				++i;
			}

			mFile << "<br>";
		}		
		mFile << "--------------------------------------------------" << "<br>";
	}

protected:
	ofstream mFile;
	LoggingLevel mLoggingLevel;
	String mOriginalLogFilename;
	StyleList mStyleList;
	StringMap mAdditionalInfo;
	int mNumEntries;
};

#endif
Fun Fact: I wrote a version of this that output to RTF format before I realized "oh yeah, html".

Edit:
You may notice a shotgun scattering of "const" throughout the file. That's because I've never really bothered to "const" things before and I'm still a little shakey on where and when and why to do that. I meant to check that all over before I posted this, but forgot so here you go.
User avatar
:wumpus:
OGRE Retired Team Member
OGRE Retired Team Member
Posts: 3067
Joined: Tue Feb 10, 2004 12:53 pm
Location: The Netherlands
x 1

Post by :wumpus: »

Looks very nice, wiki please :-) Btw the name "renderer" is a bit wrong as it is no html renderer but more like an exporter.
User avatar
Kencho
OGRE Retired Moderator
OGRE Retired Moderator
Posts: 4011
Joined: Fri Sep 19, 2003 6:28 pm
Location: Burgos, Spain
x 2
Contact:

Post by Kencho »

/me worships johnpus
Image
User avatar
sinbad
OGRE Retired Team Member
OGRE Retired Team Member
Posts: 19269
Joined: Sun Oct 06, 2002 11:19 pm
Location: Guernsey, Channel Islands
x 66
Contact:

Post by sinbad »

Heh, that's rather pretty :)
User avatar
Alexis
Halfling
Posts: 59
Joined: Fri Jul 22, 2005 3:35 pm
Location: Ukraine
x 1

Post by Alexis »

This thing looks very good. It similar to ours old 3D engine logging type :wink:
I have added this logger to my application in this way:

Code: Select all

bool ExampleApplication::setup(void)
{
    mRoot = new Root();    
    LogManager::getSingleton().addListener(new HTMLLogRenderer("log.html","styles_log.cfg",StringList()));
//....
}
It creates log.html but there are no iformation in it.
So I have no idea how to use it?
Can I redirect all log information to this HTMLLogRenderer?
User avatar
discipline
OGRE Community Helper
OGRE Community Helper
Posts: 766
Joined: Mon May 16, 2005 12:09 am

Post by discipline »

:wumpus: wrote:Btw the name "renderer" is a bit wrong as it is no html renderer but more like an exporter.
How about a target? Exporter is better than renderer, but I think target is more accurate. We can have a text file remote syslog server, html file, stdout, stderr, etc.
User avatar
Chris Jones
Lich
Posts: 1742
Joined: Tue Apr 05, 2005 1:11 pm
Location: Gosport, South England
x 1

Post by Chris Jones »

cant you just call it HTMLlog, without having renderer or exporter or target etc on the end? seeing as a normal log is just called a log...
User avatar
johnhpus
Platinum Sponsor
Platinum Sponsor
Posts: 1186
Joined: Sat Apr 17, 2004 2:49 am
x 3

Post by johnhpus »

Oops, forgot I ever posted this. Yeah, I see now that calling it "renderer" is inaccurate. Target does indeed sound like the best name, so if no one has any other suggestions I'll change it to 'Target' and add it to the wiki.

Alexis:
I'm not sure why your not seeing your message come through. I do see one problem with your code though. You need to keep a copy of the pointer to (what is for now) HTMLLogRenderer, so that you can remove it from the LogManager and delete it when the application closes.

Has anyone else had any problems / used this successfully?
User avatar
Alexis
Halfling
Posts: 59
Joined: Fri Jul 22, 2005 3:35 pm
Location: Ukraine
x 1

Post by Alexis »

The problem was in this line

Code: Select all

if( StringUtilEx::areStringsEqual( name, mOriginalLogFilename ) ) 
I don'y know why, but you should use the same name for your htmlRenderer as default log:

Code: Select all

m_html_log = new HTMLLogRenderer("Ogre.log","styles_log.cfg",StringList());
Now it work and I can see red errors. :P

But don't forget to add this line to your styles_log.cfg

Code: Select all

style=Error red white 
because with this

Code: Select all

style=Error: red white 
it would paint in red only line that begin with Error:
Thanks.
User avatar
johnhpus
Platinum Sponsor
Platinum Sponsor
Posts: 1186
Joined: Sat Apr 17, 2004 2:49 am
x 3

Post by johnhpus »

Someone sent me a private message on the forum asking if it was ok to use this code and whether I would need to be given credit for it. In case anyone else should happen to find themselves with that question, here's the answer I sent back.
Oh, I'm sorry. I should have made that clear. That code is free for anyone to use in any way they want. Also, there's no need to credit me in any way for writing it.

Good luck and let me know when your project is finished. I always enjoy seeing the work of other Ogre users.
And yeah, before too long, I'll clean this up and rename the class and get it on the Wiki.
User avatar
jacmoe
OGRE Retired Moderator
OGRE Retired Moderator
Posts: 20570
Joined: Thu Jan 22, 2004 10:13 am
Location: Denmark
x 179
Contact:

Re: HTMLLogRenderer (with screenshot)

Post by jacmoe »

It's not on the wiki yet. :)
/* Less noise. More signal. */
Ogitor Scenebuilder - powered by Ogre, presented by Qt, fueled by Passion.
OgreAddons - the Ogre code suppository.
User avatar
johnhpus
Platinum Sponsor
Platinum Sponsor
Posts: 1186
Joined: Sat Apr 17, 2004 2:49 am
x 3

Re: HTMLLogRenderer (with screenshot)

Post by johnhpus »

Holy cow. Almost six years old, I shudder to think what the code must look like!

Edit:

I guess it's not too late to keep my word on that... and I can hide the evidence that I ever wrote a function to compare two std::strings for equivalence ;)
User avatar
jacmoe
OGRE Retired Moderator
OGRE Retired Moderator
Posts: 20570
Joined: Thu Jan 22, 2004 10:13 am
Location: Denmark
x 179
Contact:

Re: HTMLLogRenderer (with screenshot)

Post by jacmoe »

/* Less noise. More signal. */
Ogitor Scenebuilder - powered by Ogre, presented by Qt, fueled by Passion.
OgreAddons - the Ogre code suppository.
Post Reply