dfd893ed6eb311a9b7c8844db7fec58642083c39 angie Fri Jan 9 15:46:14 2015 -0800 New toy CGI hgChooseDb based on PPT mockup and ideas from Ann and Matt: popular species icons and autocomplete search for species, as an alternative to hgGateway's menus. Uses the new ReactJS/ImmutableJS framework. hgChooseDb.c has three modes of operation: - HTML output for simple main page with a <div> container to be filled in by javascript - cart-based JSON responses to ajax requests from javascript (using hg/lib/cartJson.c) - no cart; fast JSON responses to species-search autocomplete requests hgChooseDb.jsx is the React/JSX UI view code, compiled to bundle/reactHgChooseDb.js. hgChooseDbModel.js is a subclass of js/model/lib/ImModel.js that gets initial state from the server and then responds to user actions. diff --git src/hg/hgChooseDb/hgChooseDb.c src/hg/hgChooseDb/hgChooseDb.c new file mode 100644 index 0000000..ef21448 --- /dev/null +++ src/hg/hgChooseDb/hgChooseDb.c @@ -0,0 +1,287 @@ +/* hgChooseDb - auto-complete db/organism search */ +#include "common.h" +#include "cart.h" +#include "cartJson.h" +#include "cheapcgi.h" +#include "hCommon.h" +#include "hdb.h" +#include "hui.h" +#include "jsonParse.h" +#include "obscure.h" // for readInGulp +#include "trackHub.h" +#include "web.h" + +/* Global Variables */ +struct cart *cart = NULL; /* CGI and other variables */ + +#define SEARCH_TERM "hgcd_term" + +static char *getPhotoForGenome(char *genome, char *db) +/* If the expected file for this genome's description page photo exists, return the filename. */ +{ +if (db == NULL) + db = hDefaultDbForGenome(genome); +char baseName[PATH_LEN]; +if (sameString("Human", genome)) + safecpy(baseName, sizeof(baseName), "human.jpg"); +else + { + safef(baseName, sizeof(baseName), "%s.jpg", hScientificName(db)); + subChar(baseName, ' ', '_'); + } +char fileName[PATH_LEN]; +safef(fileName, sizeof(fileName), "%s/images/%s", hDocumentRoot(), baseName); +if (fileExists(fileName)) + { + // Reformat for URL: + safef(fileName, sizeof(fileName), "../images/%s", baseName); + return cloneString(fileName); + } +else + return NULL; +} + +static void writeGenomeInfo(struct jsonWrite *jw, char *genome, char *db) +// Write menu options, selected db etc. for genome and db. If db is NULL use default for genome. +{ +if (db == NULL) + db = hDefaultDbForGenome(genome); +struct slPair *dbOptions = hGetDbOptionsForGenome(genome); +jsonWriteValueLabelList(jw, "dbOptions", dbOptions); +jsonWriteString(jw, "db", db); +jsonWriteString(jw, "genome", genome); +jsonWriteString(jw, "img", getPhotoForGenome(genome, db)); +} + +static void writeDbMenuData(struct jsonWrite *jw, char *genome, char *db) +// Set dbMenuData to use genome's info. +{ +jsonWriteObjectStart(jw, "dbMenuData"); +writeGenomeInfo(jw, genome, db); +jsonWriteObjectEnd(jw); +} + +static void getDbMenu(struct cartJson *cj, struct hash *paramHash) +/* Write a dbMenu for the selected search term match. */ +{ +char *genome = cartJsonRequiredParam(paramHash, "genome", cj->jw, "getDbMenu"); +char *db = cartJsonOptionalParam(paramHash, "db"); +writeDbMenuData(cj->jw, genome, db); +} + +static char *popularSpecies[] = + { "Human", "Mouse", "Rat", "D. melanogaster", "C. elegans", NULL }; + +static void writeOnePopularSpecies(struct jsonWrite *jw, char *genome, char *db) +// Write a nameless object with info about this species. +{ +jsonWriteObjectStart(jw, NULL); +writeGenomeInfo(jw, genome, db); +jsonWriteObjectEnd(jw); +} + +static void getPopularSpecies(struct cartJson *cj, struct hash *paramHash) +/* Return a list of popular species' names, images and db menu info. */ +{ +jsonWriteListStart(cj->jw, "popularSpecies"); +char *currentDb = cartOptionalString(cart, "db"); +char *currentGenome = currentDb ? hGenome(currentDb) : NULL; +boolean gotCurrent = FALSE; +int i; +for (i = 0; popularSpecies[i] != NULL; i++) + { + char *db = NULL; + if (sameOk(popularSpecies[i], currentGenome)) + { + gotCurrent = TRUE; + db = currentDb; + } + writeOnePopularSpecies(cj->jw, popularSpecies[i], db); + } +if (! gotCurrent) + writeOnePopularSpecies(cj->jw, currentGenome, currentDb); +jsonWriteListEnd(cj->jw); +if (currentGenome != NULL) + writeDbMenuData(cj->jw, currentGenome, currentDb); +} + +static void getDescriptionHtml(struct cartJson *cj, struct hash *paramHash) +/* Return assembly description html for the given db. */ +{ +char *db = cartJsonRequiredParam(paramHash, "db", cj->jw, "getDescriptionHtml"); +char *htmlPath = hHtmlPath(db); +char *htmlString = NULL; +if (htmlPath != NULL) + { + if (fileExists(htmlPath)) + readInGulp(htmlPath, &htmlString, NULL); + else if ( startsWith("http://" , htmlPath) || + startsWith("https://", htmlPath) || + startsWith("ftp://" , htmlPath)) + { + struct lineFile *lf = udcWrapShortLineFile(htmlPath, NULL, 256*1024); + htmlString = lineFileReadAll(lf); + lineFileClose(&lf); + } + } +if (isNotEmpty(htmlString)) + { +//#*** TODO: move jsonStringEscape inside jsonWriteString + htmlString = jsonStringEscape(htmlString); + jsonWriteObjectStart(cj->jw, "assemblyDescription"); + jsonWriteString(cj->jw, "db", db); + jsonWriteString(cj->jw, "description", htmlString); + jsonWriteObjectEnd(cj->jw); + } +} + +static void doCartJson() +/* Perform UI commands to update the cart and/or retrieve cart vars & metadata. */ +{ +struct cartJson *cj = cartJsonNew(cart); +cartJsonRegisterHandler(cj, "getPopularSpecies", getPopularSpecies); +cartJsonRegisterHandler(cj, "getDbMenu", getDbMenu); +cartJsonRegisterHandler(cj, "getDescriptionHtml", getDescriptionHtml); +cartJsonExecute(cj); +} + +static void doMainPage() +/* Send HTML with javascript to bootstrap the user interface. */ +{ + +//#*** A lot of this is copied from hgAi... libify! + +char *db = cartUsualString(cart, "db", hDefaultDb()); +webStartWrapperDetailedNoArgs(cart, trackHubSkipHubName(db), + "", "UCSC Genome Browser Databases", + TRUE, FALSE, TRUE, TRUE); + +// Ideally these would go in the <HEAD> +puts("<link rel=\"stylesheet\" href=\"//code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css\">"); +puts("<link rel=\"stylesheet\" href=\"//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css\">"); + +puts("<div id=\"appContainer\">Loading...</div>"); + +// Set a global JS variable hgsid. +// Plain old "var ..." doesn't work (other scripts can't see it), it has to belong to window. +printf("<script>window.%s='%s';</script>\n", cartSessionVarName(), cartSessionId(cart)); + +// We need a package manager and require-handling system... bower and browserify? +puts("<script src=\"../js/es5-shim.4.0.3.min.js\"></script>"); +puts("<script src=\"../js/es5-sham.4.0.3.min.js\"></script>"); +puts("<script src=\"../js/lodash.2.4.1.compat.min.js\"></script>"); +puts("<script src=\"//code.jquery.com/jquery-1.9.1.min.js\"></script>"); +puts("<script src=\"//code.jquery.com/ui/1.10.3/jquery-ui.min.js\"></script>"); +puts("<script src=\"//fb.me/react-with-addons-0.12.2.min.js\"></script>"); +puts("<script src=\"../js/immutable.3.2.1.min.js\"></script>"); +puts("<script src=\"../js/BackboneExtend.js\"></script>"); +puts("<script src=\"../js/cart.js\"></script>"); +puts("<script src=\"../js/ImModel.js\"></script>"); +puts("<script src=\"../js/PathUpdate.js\"></script>"); +puts("<script src=\"../js/PathUpdateOptional.js\"></script>"); +puts("<script src=\"../js/ImmutableUpdate.js\"></script>"); +puts("<script src=\"../js/reactLibBundle.js\"></script>"); +puts("<script src=\"../js/reactHgChooseDb.js\"></script>"); +puts("<script src=\"../js/hgChooseDbModel.js\"></script>"); + +// Invisible form for jumping to hgTracks +printf("\n<form action=\"%s\" method=%s id='mainForm'>\n", + hgTracksName(), cartUsualString(cart, "formMethod", "GET")); +cartSaveSession(cart); +cgiMakeHiddenVar("db", db); +puts("</form>"); +webEnd(); +} + +void doMiddle(struct cart *theCart) +/* Depending on invocation, either perform a query and print out results + * or display the main page. */ +{ +cart = theCart; +if (cgiOptionalString(CARTJSON_COMMAND)) + doCartJson(); +else + doMainPage(); +} + +static void fail(char *msg) +//#*** Copied from hgSuggest... libify to cheapCgi? +{ +puts("Status: 400\n\n"); +puts(msg); +exit(-1); +} + +INLINE void addIfStartsWithNoCase(char *term, char *target, struct dbDb *dbDb, + struct hash *matchHash) +{ +if (startsWithNoCase(term, target)) + { + if (! hashLookup(matchHash, target)) + hashAdd(matchHash, target, dbDb); + } +} + +static void lookupTerm() +/* Look for matches to term in hgCentral and print as JSON if found. */ +{ +char *term = cgiOptionalString(SEARCH_TERM); +if (isEmpty(term)) + fail("Missing search term parameter"); + +// Write JSON response with list of matches +puts("Content-Type:text/javascript\n"); +struct jsonWrite *jw = jsonWriteNew(); +jsonWriteListStart(jw, NULL); + +// Search dbDb for matches +struct hash *matchHash = hashNew(0); +struct dbDb *dbDbList = hDbDbList(), *dbDb; +for (dbDb = dbDbList; dbDb != NULL; dbDb = dbDb->next) + { + if (startsWithNoCase(term, dbDb->name)) + { + jsonWriteObjectStart(jw, NULL); + char description[PATH_LEN]; + safef(description, sizeof(description), "%s (%s %s)", + dbDb->name, dbDb->genome, dbDb->description); + jsonWriteString(jw, "value", description); + jsonWriteString(jw, "genome", dbDb->genome); + jsonWriteString(jw, "db", dbDb->name); + jsonWriteObjectEnd(jw); + } + addIfStartsWithNoCase(term, dbDb->genome, dbDb, matchHash); + addIfStartsWithNoCase(term, dbDb->scientificName, dbDb, matchHash); + addIfStartsWithNoCase(term, dbDb->sourceName, dbDb, matchHash); + } + +struct hashEl *hel; +struct hashCookie cookie = hashFirst(matchHash); +while ((hel = hashNext(&cookie)) != NULL) + { + dbDb = hel->val; + jsonWriteObjectStart(jw, NULL); + jsonWriteString(jw, "value", hel->name); + jsonWriteString(jw, "genome", dbDb->genome); + jsonWriteObjectEnd(jw); + } +jsonWriteListEnd(jw); +puts(jw->dy->string); +} + +int main(int argc, char *argv[]) +/* Process CGI / command line. */ +{ +/* Null terminated list of CGI Variables we don't want to save + * permanently. */ +char *excludeVars[] = {SEARCH_TERM, CARTJSON_COMMAND, NULL,}; +struct hash *oldVars = NULL; +cgiSpoof(&argc, argv); +setUdcCacheDir(); +if (cgiOptionalString(SEARCH_TERM)) + // Skip the cart for speedy searches + lookupTerm(); +else + cartEmptyShellNoContent(doMiddle, hUserCookie(), excludeVars, oldVars); +return 0; +}