You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tdegraphics/kmrml/kmrml/mrml_part.cpp

858 lines
26 KiB

/* This file is part of the KDE project
Copyright (C) 2001,2002 Carsten Pfeiffer <pfeiffer@kde.org>
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public
License as published by the Free Software Foundation, version 2.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; see the file COPYING. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
#include <tqcheckbox.h>
#include <tqcursor.h>
#include <tqdir.h>
#include <tqfile.h>
#include <tqgrid.h>
#include <tqhgroupbox.h>
#include <tqlabel.h>
#include <tqpushbutton.h>
#include <tqtooltip.h>
#include <tqvbox.h>
#include <kaboutdata.h>
#include <kapplication.h>
#include <kcombobox.h>
#include <kconfig.h>
#include <kdatastream.h>
#include <kdebug.h>
#include <kglobal.h>
#include <klocale.h>
#include <kiconloader.h>
#include <kinstance.h>
#include <kio/job.h>
#include <kio/jobclasses.h>
#include <kmessagebox.h>
#include <knuminput.h>
#include <kprotocolinfo.h>
#include <kparts/genericfactory.h>
#include <ktempfile.h>
#include <mrml_utils.h>
#include "algorithmdialog.h"
#include "browser.h"
#include "collectioncombo.h"
#include "mrml_creator.h"
#include "mrml_elements.h"
#include "mrml_shared.h"
#include "mrml_view.h"
#include "mrml_part.h"
#include "version.h"
using namespace KMrml;
extern "C"
{
void * init_libkmrmlpart() {
return new KMrml::PartFactory();
}
}
TDEInstance * PartFactory::s_instance = 0L;
PartFactory::PartFactory()
: KParts::Factory()
{
MrmlShared::ref();
}
PartFactory::~PartFactory()
{
MrmlShared::deref();
delete s_instance;
s_instance = 0L;
}
TDEInstance * PartFactory::instance()
{
if ( !s_instance ) {
s_instance = new TDEInstance( "kmrml" );
TDEGlobal::locale()->insertCatalogue( "kmrml" );
}
return s_instance;
}
KParts::Part * PartFactory::createPartObject( TQWidget *parentWidget,
const char *widgetName,
TQObject *parent,
const char *name,
const char *,
const TQStringList& args )
{
return new MrmlPart( parentWidget, widgetName, parent, name, args );
}
// can't use this due to MrmlShared ref-counting
// typedef KParts::GenericFactory<KMrml::MrmlPart> PartFactory;
// K_EXPORT_COMPONENT_FACTORY( mrmlpart, PartFactory )
///////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////
uint MrmlPart::s_sessionId = 0;
MrmlPart::MrmlPart( TQWidget *parentWidget, const char * /* widgetName */,
TQObject *parent, const char *name,
const TQStringList& /* args */ )
: KParts::ReadOnlyPart( parent, name ),
m_job( 0L ),
m_status( NeedCollection )
{
m_sessionId = TQString::number( s_sessionId++ ).prepend("kmrml_");
setName( "MRML Part" );
m_browser = new Browser( this, "mrml browserextension");
setInstance( PartFactory::instance(), true ); // do load plugins :)
KConfig *config = PartFactory::instance()->config();
config->setGroup("MRML Settings");
TQVBox *box = new TQVBox( parentWidget, "main mrml box" );
m_view = new MrmlView( box, "MrmlView" );
connect( m_view, TQT_SIGNAL( activated( const KURL&, ButtonState )),
this, TQT_SLOT( slotActivated( const KURL&, ButtonState )));
connect( m_view, TQT_SIGNAL( onItem( const KURL& )),
this, TQT_SLOT( slotSetStatusBar( const KURL& )));
m_panel = new TQHGroupBox( box, "buttons box" );
TQGrid *comboGrid = new TQGrid( 2, m_panel, "combo grid" );
comboGrid->setSpacing( KDialog::spacingHint() );
(void) new TQLabel( i18n("Server to query:"), comboGrid );
m_hostCombo = new KComboBox( false, comboGrid, "host combo" );
initHostCombo();
connect( m_hostCombo, TQT_SIGNAL( activated( const TQString& ) ),
TQT_SLOT( slotHostComboActivated( const TQString& )));
(void) new TQLabel( i18n("Search in collection:"), comboGrid );
m_collectionCombo = new CollectionCombo( comboGrid, "collection-combo" );
// will be re-set in initCollections(), but we need to set it here to
// prevent crashes when the connection to the server fails
m_collectionCombo->setCollections( &m_collections );
m_algoButton = new TQPushButton( TQString(), m_panel );
m_algoButton->setPixmap( SmallIcon("configure") );
m_algoButton->setFixedSize( m_algoButton->sizeHint() );
connect( m_algoButton, TQT_SIGNAL( clicked() ),
TQT_SLOT( slotConfigureAlgorithm() ));
TQToolTip::add( m_algoButton, i18n("Configure algorithm") );
TQWidget *spacer = new TQWidget( m_panel );
spacer->setSizePolicy( TQSizePolicy( TQSizePolicy::MinimumExpanding,
TQSizePolicy::Minimum ) );
int resultSize = config->readNumEntry( "Result-size", 20 );
m_resultSizeInput = new KIntNumInput( resultSize, m_panel );
m_resultSizeInput->setRange( 1, 100 );
m_resultSizeInput->setLabel( i18n("Maximum result images:") );
TQVBox *tmp = new TQVBox( m_panel );
m_random = new TQCheckBox( i18n("Random search"), tmp );
m_startButton = new TQPushButton( TQString(), tmp );
connect( m_startButton, TQT_SIGNAL( clicked() ), TQT_SLOT( slotStartClicked() ));
setStatus( NeedCollection );
setWidget( box );
// setXMLFile( "mrml_part.rc" );
slotSetStatusBar( TQString() );
enableServerDependentWidgets( false );
}
MrmlPart::~MrmlPart()
{
closeURL();
}
void MrmlPart::enableServerDependentWidgets( bool enable )
{
m_collectionCombo->setEnabled( enable );
m_algoButton->setEnabled( enable && false ); // ### re-enable!!!
}
void MrmlPart::initCollections( const TQDomElement& elem )
{
m_collections.initFromDOM( elem );
m_collectionCombo->setCollections( &m_collections );
enableServerDependentWidgets( m_collectionCombo->count() > 0 );
if ( m_collectionCombo->count() == 0 )
{
KMessageBox::information( widget(),
i18n("There is no image collection available\n"
"at %1.\n"), i18n("No Image Collection"));
setStatus( NeedCollection );
}
else
m_collectionCombo->updateGeometry(); // adjust the entire grid
}
void MrmlPart::initAlgorithms( const TQDomElement& elem )
{
m_algorithms.initFromDOM( elem );
}
// this is where we start!
bool MrmlPart::openURL( const KURL& url )
{
closeURL();
if ( url.protocol() != "mrml" || !url.isValid() ) {
tqWarning("MrmlPart::openURL: cannot handle url: %s", url.prettyURL().latin1());
return false; // what to do with that?
}
m_url = url;
TQString host = url.host().isEmpty() ?
TQString::fromLatin1("localhost") : url.host();
m_hostCombo->setCurrentItem( host );
// urls we need to download before starting the query
KURL::List downloadList;
m_queryList.clear();
TQString param = url.queryItem( "relevant" );
TQStringList list = TQStringList::split( ';', param );
// we can only search by example on localhost
if ( host != "localhost" )
{
if ( !list.isEmpty() )
KMessageBox::sorry( m_view,
i18n("You can only search by example images "
"on a local indexing server."),
i18n("Only Local Servers Possible") );
}
else // localhost query
{
for( TQStringList::Iterator it = list.begin(); it != list.end(); ++it )
{
KURL u;
if ( (*it).at(0) == '/' )
u.setPath( *it );
else
u = *it;
if ( u.isValid() )
{
if ( u.isLocalFile() )
m_queryList.append( u );
else
downloadList.append( u );
}
}
// ### we need a real solution for this!
// gift refuses to start when no config file is available.
if ( !TQFile::exists( m_config.mrmldDataDir() + "/gift-config.mrml" ) )
{
if ( KMessageBox::questionYesNo(0L,
i18n("There are no indexable folders "
"specified. Do you want to configure them "
"now?"),
i18n("Configuration Missing"),
i18n("Configure"),
i18n("Do Not Configure"),
"kmrml_ask_configure_gift" )
== KMessageBox::Yes )
{
TDEApplication::tdeinitExec( "kcmshell",
TQString::fromLatin1("kcmkmrml"));
setStatus( NeedCollection );
return false;
}
}
}
if ( !downloadList.isEmpty() )
downloadReferenceFiles( downloadList );
else
contactServer( m_url );
return true;
}
void MrmlPart::contactServer( const KURL& url )
{
m_job = transferJob( url );
m_job->addMetaData( MrmlShared::kio_task(), MrmlShared::kio_initialize() );
TQString host = url.host().isEmpty() ?
TQString::fromLatin1("localhost") : url.host();
slotSetStatusBar( i18n("Connecting to indexing server at %1...").arg( host ));
}
//
// schedules a download all urls of downloadList (all remote and wellformed)
// No other downloads are running (closeURL() has been called before)
//
void MrmlPart::downloadReferenceFiles( const KURL::List& downloadList )
{
assert( m_downloadJobs.isEmpty() );
KURL::List::ConstIterator it = downloadList.begin();
for ( ; it != downloadList.end(); it++ )
{
TQString extension;
int index = (*it).fileName().findRev( '.' );
if ( index != -1 )
extension = (*it).fileName().mid( index );
KTempFile tmpFile( TQString(), extension );
if ( tmpFile.status() != 0 )
{
kdWarning() << "Can't create temporary file, skipping: " << *it << endl;
continue;
}
m_tempFiles.append( tmpFile.name() );
KURL destURL;
destURL.setPath( tmpFile.name() );
KIO::FileCopyJob *job = KIO::file_copy( *it, destURL, -1,
true /* overwrite tmpfile */ );
connect( job, TQT_SIGNAL( result( KIO::Job * ) ),
TQT_SLOT( slotDownloadResult( KIO::Job * ) ));
m_downloadJobs.append( job );
// ### should this be only called for one job?
emit started( job );
}
if ( !m_downloadJobs.isEmpty() )
slotSetStatusBar( i18n("Downloading reference files...") );
else // probably never happens
contactServer( m_url );
}
bool MrmlPart::closeURL()
{
m_view->stopDownloads();
m_view->clear();
TQPtrListIterator<KIO::FileCopyJob> it( m_downloadJobs );
for ( ; it.current(); ++it )
it.current()->kill();
m_downloadJobs.clear();
TQStringList::Iterator tit = m_tempFiles.begin();
for ( ; tit != m_tempFiles.end(); ++tit )
TQFile::remove( *tit );
m_tempFiles.clear();
if ( m_job ) {
m_job->kill();
m_job = 0L;
}
setStatus( NeedCollection );
return true;
}
KIO::TransferJob * MrmlPart::transferJob( const KURL& url )
{
KIO::TransferJob *job = KIO::get( url, true, false ); // reload, no gui
job->setAutoErrorHandlingEnabled( true, m_view );
connect( job, TQT_SIGNAL( result( KIO::Job * )),
TQT_SLOT( slotResult( KIO::Job * )));
connect( job, TQT_SIGNAL( data( KIO::Job *, const TQByteArray& )),
TQT_SLOT( slotData( KIO::Job *, const TQByteArray& )));
// ###
// connect( job, TQT_SIGNAL( infoMessage( KIO::Job *, const TQString& )),
// TQT_SLOT( slotResult( KIO::Job *, const TQString& )));
job->setWindow( widget() );
if ( !m_sessionId.isEmpty() )
job->addMetaData( MrmlShared::sessionId(), m_sessionId );
emit started( job );
emit setWindowCaption( url.prettyURL() );
setStatus( InProgress );
return job;
}
void MrmlPart::slotResult( KIO::Job *job )
{
if ( job == m_job )
m_job = 0L;
slotSetStatusBar( TQString() );
if ( !job->error() )
emit completed();
else {
emit canceled( job->errorString() );
// tqDebug("*** canceled: error: %s", job->errorString().latin1());
}
bool auto_random = m_view->isEmpty() && m_queryList.isEmpty();
m_random->setChecked( auto_random );
m_random->setEnabled( !auto_random );
setStatus( job->error() ? NeedCollection : CanSearch );
if ( !job->error() && !m_queryList.isEmpty() ) {
// we have a connection and we got a list of relevant URLs to query for
// (via the URL)
createQuery( &m_queryList );
m_queryList.clear();
}
}
// ### when user cancels download, we crash :(
void MrmlPart::slotDownloadResult( KIO::Job *job )
{
assert( job->inherits( "KIO::FileCopyJob" ) );
KIO::FileCopyJob *copyJob = static_cast<KIO::FileCopyJob*>( job );
if ( !copyJob->error() )
m_queryList.append( copyJob->destURL() );
m_downloadJobs.removeRef( copyJob );
if ( m_downloadJobs.isEmpty() ) // finally, we can start the query!
{
if ( m_queryList.isEmpty() ) // rather unlikely, but could happen ;)
{
kdWarning() << "Couldn't download the reference files. Will start a random search now" << endl;
}
contactServer( m_url );
}
}
// mrml-document in the bytearray
void MrmlPart::slotData( KIO::Job *, const TQByteArray& data )
{
if ( data.isEmpty() )
return;
TQDomDocument doc;
doc.setContent( data );
if ( !doc.isNull() )
parseMrml( doc );
}
void MrmlPart::parseMrml( TQDomDocument& doc )
{
TQDomNode mrml = doc.documentElement(); // root element
if ( !mrml.isNull() ) {
TQDomNode child = mrml.firstChild();
for ( ; !child.isNull(); child = child.nextSibling() ) {
// tqDebug("**** HERE %s", child.nodeName().latin1());
if ( child.isElement() ) {
TQDomElement elem = child.toElement();
TQString tagName = elem.tagName();
if ( tagName == "acknowledge-session-op" )
m_sessionId = elem.attribute( MrmlShared::sessionId() );
else if ( tagName == MrmlShared::algorithmList() ) {
initAlgorithms( elem );
}
else if ( tagName == MrmlShared::collectionList() ) {
initCollections( elem );
}
else if ( tagName == "error" ) {
KMessageBox::information( widget(),
i18n("Server returned error:\n%1\n")
.arg( elem.attribute( "message" )),
i18n("Server Error") );
}
else if ( tagName == "query-result" ) {
m_view->clear();
parseQueryResult( elem );
}
} // child.isElement()
}
} // !mrml.isNull()
}
void MrmlPart::parseQueryResult( TQDomElement& queryResult )
{
TQDomNode child = queryResult.firstChild();
for ( ; !child.isNull(); child = child.nextSibling() ) {
if ( child.isElement() ) {
TQDomElement elem = child.toElement();
TQString tagName = elem.tagName();
if ( tagName == "query-result-element-list" ) {
TQValueList<TQDomElement> list =
KMrml::directChildElements( elem, "query-result-element" );
TQValueListConstIterator<TQDomElement> it = list.begin();
for ( ; it != list.end(); ++it )
{
TQDomNamedNodeMap a = (*it).attributes();
m_view->addItem( KURL( (*it).attribute("image-location" ) ),
KURL( (*it).attribute("thumbnail-location" ) ),
(*it).attribute("calculated-similarity"));
}
}
else if ( tagName == "query-result" )
parseQueryResult( elem );
}
}
}
// creates/stops the query when the Start/Stop button was pressed
void MrmlPart::slotStartClicked()
{
if ( m_status == InProgress )
{
closeURL();
m_startButton->setText( i18n("&Search" ) );
return;
}
// we need to reconnect, if the initial openURL() didn't work due to
// the gift not being available.
if ( m_status == NeedCollection )
{
openURL( m_url );
return;
}
// cut off an eventual query and reference from the url, when the user
// performs a real query (otherwise restoreState() would restore and
// re-do the query from the URL
m_url.setRef( TQString() );
m_url.setQuery( TQString() );
createQuery();
m_browser->openURLNotify();
}
//
// relevantItems is 0L when called from slotStartClicked() and set to a
// non-empty list when called initially, from the commandline.
//
void MrmlPart::createQuery( const KURL::List * relevantItems )
{
if ( relevantItems && relevantItems->isEmpty() )
return;
TQDomDocument doc( "mrml" );
TQDomElement mrml = MrmlCreator::createMrml( doc,
sessionId(),
transactionId() );
Collection coll = currentCollection();
// tqDebug("** collection: name: %s, id: %s, valid: %i", coll.name().latin1(), coll.id().latin1(), coll.isValid());
Algorithm algo = firstAlgorithmForCollection( coll );
// tqDebug("** algorithm: name: %s, id: %s, valid: %i, collection-id: %s", algo.name().latin1(), algo.id().latin1(), algo.isValid(), algo.collectionId().latin1());
if ( algo.isValid() )
{
MrmlCreator::configureSession( mrml, algo, sessionId() );
}
TQDomElement query = MrmlCreator::addQuery( mrml,
m_resultSizeInput->value() );
if ( algo.isValid() )
query.setAttribute( MrmlShared::algorithmId(), algo.id() );
// ### result-cutoff, query-type?
// start-up with/without urls on the commandline via mrmlsearch
if ( relevantItems )
{
TQDomElement elem = MrmlCreator::addRelevanceList( query );
KURL::List::ConstIterator it = relevantItems->begin();
for ( ; it != relevantItems->end(); ++it )
MrmlCreator::createRelevanceElement( doc, elem, (*it).url(),
MrmlCreator::Relevant );
}
// get relevant items from the view? Only do this when relevantItems is 0L
else if ( !m_random->isChecked() )
{
TQDomElement relevants = MrmlCreator::addRelevanceList( query );
m_view->addRelevanceToQuery( doc, relevants );
}
performQuery( doc );
}
Collection MrmlPart::currentCollection() const
{
return m_collectionCombo->current();
}
Algorithm MrmlPart::firstAlgorithmForCollection( const Collection& coll ) const
{
if ( !m_algorithms.isEmpty() )
{
AlgorithmList::ConstIterator it = m_algorithms.begin();
for ( ; it != m_algorithms.end(); ++it )
{
Algorithm algo = *it;
if ( algo.paradigms().matches( coll.paradigms() ) )
{
algo.setCollectionId( coll.id() );
return algo;
}
}
}
tqDebug("#################### -> ADEFAULT!");
Algorithm algo = Algorithm::defaultAlgorithm();
algo.setCollectionId( coll.id() );
return algo;
}
// emits the given TQDomDocument for eventual plugins, checks after that
// if there are any relevance elements. If there are none, random search is
// implied and performed.
// finally, the search is actually started
void MrmlPart::performQuery( TQDomDocument& doc )
{
TQDomElement mrml = doc.documentElement();
emit aboutToStartQuery( doc ); // let plugins play with it :)
// no items available? All "neutral"? -> random search
TQDomElement queryStep = KMrml::firstChildElement( mrml, "query-step" );
bool randomSearch = false;
if ( !queryStep.isNull() )
{
TQDomElement relevanceList =
KMrml::firstChildElement(queryStep, "user-relevance-element-list");
TQValueList<TQDomElement> relevanceElements =
KMrml::directChildElements( relevanceList,
"user-relevance-element" );
randomSearch = relevanceElements.isEmpty();
if ( randomSearch )
{
m_random->setChecked( true );
m_random->setEnabled( false );
queryStep.setAttribute("query-type", "at-random");
// remove user-relevance-element-list element for random search
relevanceList.parentNode().removeChild( relevanceList );
}
}
else
{
KMessageBox::error( m_view, i18n("Error formulating the query. The "
"\"query-step\" element is missing."),
i18n("Query Error") );
}
m_job = transferJob( url() );
slotSetStatusBar( randomSearch ? i18n("Random search...") :
i18n("Searching...") );
m_job->addMetaData( MrmlShared::kio_task(), MrmlShared::kio_startQuery() );
tqDebug("\n\nSending XML:\n%s", doc.toString().latin1());
m_job->addMetaData( MrmlShared::mrml_data(), doc.toString() );
}
void MrmlPart::slotSetStatusBar( const TQString& text )
{
if ( text.isEmpty() )
emit setStatusBarText( i18n("Ready.") );
else
emit setStatusBarText( text );
}
void MrmlPart::slotActivated( const KURL& url, ButtonState button )
{
if ( button == Qt::LeftButton )
emit m_browser->openURLRequest( url );
else if ( button == Qt::MidButton )
emit m_browser->createNewWindow( url );
else if ( button == Qt::RightButton ) {
// enableExtensionActions( url, true ); // for now
emit m_browser->popupMenu( TQCursor::pos(), url, TQString() );
// enableExtensionActions( url, false );
}
}
void MrmlPart::enableExtensionActions( const KURL& url, bool enable )
{
bool del = KProtocolInfo::supportsDeleting( url );
emit m_browser->enableAction( "copy", enable );
emit m_browser->enableAction( "trash", del );
emit m_browser->enableAction( "del", del );
emit m_browser->enableAction( "shred", url.isLocalFile() );
emit m_browser->enableAction( "properties", enable );
// emit m_browser->enableAction( "print", enable ); // ### later
}
// only implemented because it's abstract in the baseclass
bool MrmlPart::openFile()
{
return false;
}
void MrmlPart::slotConfigureAlgorithm()
{
m_algoButton->setEnabled( false );
m_algoConfig = new AlgorithmDialog( m_algorithms, m_collections,
currentCollection(),
m_view, "algorithm configuration" );
connect( m_algoConfig, TQT_SIGNAL( applyClicked() ),
TQT_SLOT( slotApplyAlgoConfig() ));
connect( m_algoConfig, TQT_SIGNAL( finished() ),
TQT_SLOT( slotAlgoConfigFinished() ));
m_algoConfig->show();
}
void MrmlPart::slotApplyAlgoConfig()
{
// ###
}
void MrmlPart::slotAlgoConfigFinished()
{
if ( m_algoConfig->result() == TQDialog::Accepted )
slotApplyAlgoConfig();
m_algoButton->setEnabled( true );
m_algoConfig->deleteLater();
m_algoConfig = 0L;
}
void MrmlPart::initHostCombo()
{
m_hostCombo->clear();
m_hostCombo->insertStringList( m_config.hosts() );
}
void MrmlPart::slotHostComboActivated( const TQString& host )
{
ServerSettings settings = m_config.settingsForHost( host );
openURL( settings.getUrl() );
}
void MrmlPart::setStatus( Status status )
{
switch ( status )
{
case NeedCollection:
m_startButton->setText( i18n("&Connect") );
break;
case CanSearch:
m_startButton->setText( i18n("&Search") );
break;
case InProgress:
m_startButton->setText( i18n("Sto&p") );
break;
};
m_status = status;
}
void MrmlPart::saveState( TQDataStream& stream )
{
stream << url();
stream << m_sessionId;
stream << m_queryList;
// stream << m_algorithms;
// stream << m_collections;
stream << m_resultSizeInput->value();
stream << *m_collectionCombo;
m_view->saveState( stream );
}
void MrmlPart::restoreState( TQDataStream& stream )
{
KURL url;
stream >> url;
stream >> m_sessionId;
stream >> m_queryList;
// stream >> m_algorithms;
// stream >> m_collections;
int resultSize;
stream >> resultSize;
m_resultSizeInput->setValue( resultSize );
stream >> *m_collectionCombo;
m_view->restoreState( stream );
// openURL( url );
m_url = url;
}
TDEAboutData * MrmlPart::createAboutData()
{
TDEAboutData *data = new TDEAboutData(
"kmrml",
I18N_NOOP("MRML Client for TDE"),
KMRML_VERSION,
I18N_NOOP("A tool to search for images by their content"),
TDEAboutData::License_GPL,
I18N_NOOP("(c) 2001-2002, Carsten Pfeiffer"),
0,
I18N_NOOP("http://devel-home.kde.org/~pfeiffer/kmrml/") );
data->addAuthor( "Carsten Pfeiffer",
I18N_NOOP("Developer, Maintainer"),
"pfeiffer@kde.org" );
data->addCredit( "Wolfgang Mller",
I18N_NOOP("Developer of the GIFT, Helping Hand") );
return data;
}
///////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////
#include "mrml_part.moc"