Skip to content

Commit 5f97351

Browse files
committed
smiles working
1 parent c80eccb commit 5f97351

7 files changed

Lines changed: 122 additions & 20 deletions

File tree

package-lock.json

Lines changed: 17 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
"react-dom": "^18.2.0",
2626
"react-json-view-lite": "^2.5.0",
2727
"react-range": "^1.10.0",
28-
"react-router-dom": "^7.12.0"
28+
"react-router-dom": "^7.12.0",
29+
"smiles-drawer": "^2.1.7"
2930
},
3031
"devDependencies": {
3132
"@tailwindcss/vite": "^4.1.16",

public/cachedProviders.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@
289289
"attributes": {
290290
"name": "Physical Sciences Data Infrastructure",
291291
"description": "A namespace for properties and data related to the Physical Sciences Data Infrastructure (PSDI), UK-based integrated data instructure for the Physical Sciences.",
292-
"base_url": null,
292+
"base_url": "https://metadata.psdi.ac.uk/optimade-index-metadb",
293293
"homepage": "https://www.psdi.ac.uk",
294294
"link_type": "external"
295295
}

src/api.js

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@ import { corsProxies } from "./corsProxies.js";
22

33
import { elements } from "./components/OptimadeClient/OptimadeFilters/OptimadePTable/elements.js";
44

5-
const OFFICIAL_PROVIDERS_URL =
6-
"https://raw.githubusercontent.com/Materials-Consortia/providers/refs/heads/master/src/links/v1/providers.json";
7-
85
async function fetchWithCorsFallback(url) {
96
const attempts = [
107
{ name: "direct", url },
@@ -28,8 +25,11 @@ async function fetchWithCorsFallback(url) {
2825
}
2926

3027
// --- Providers list ---
31-
// Fetch from the Materials Consortia official list, fallback to the cached version in this repo
32-
export async function getProvidersList(excludeIds = []) {
28+
// go to optimade url, if that fails use the fallback here.
29+
export async function getProvidersList(
30+
providersUrl = "https://raw.githubusercontent.com/Materials-Consortia/providers/refs/heads/master/src/links/v1/providers.json",
31+
excludeIds = [],
32+
) {
3333
async function fetchJson(url) {
3434
const res = await fetch(url);
3535
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
@@ -38,22 +38,21 @@ export async function getProvidersList(excludeIds = []) {
3838

3939
let json;
4040
try {
41-
json = await fetchJson(OFFICIAL_PROVIDERS_URL);
41+
json = await fetchJson(providersUrl);
4242
} catch (err) {
4343
console.warn(
4444
"Remote fetch failed, falling back to cachedProviders.json:",
4545
err,
4646
);
47-
json = await fetchJson("cachedProviders.json");
47+
try {
48+
json = await fetchJson("cachedProviders.json");
49+
} catch (fallbackErr) {
50+
console.error("Both remote and local fetches failed:", fallbackErr);
51+
throw fallbackErr;
52+
}
4853
}
4954

50-
const filteredData = json.data.filter(
51-
(p) =>
52-
!excludeIds.includes(p.id) &&
53-
typeof p.attributes.base_url === "string" &&
54-
p.attributes.base_url.trim() !== "",
55-
);
56-
55+
const filteredData = json.data.filter((p) => !excludeIds.includes(p.id));
5756
return { ...json, data: filteredData };
5857
}
5958

src/components/OptimadeClient/ResultViewer.jsx

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,21 @@ import QEInputButton from "../common/QEInputButton";
77
import { containerStyleHalf } from "../../styles/containerStyles";
88

99
import { optimadeToCrystalStructure } from "../../utils";
10-
1110
import { structureToCif } from "matsci-parse";
1211

12+
import { SmilesViewer } from "../OptimadeSmilesHandler";
13+
14+
function getSmiles(attributes) {
15+
if (!attributes) return null;
16+
17+
const key = Object.keys(attributes).find((k) =>
18+
k.toLowerCase().includes("smiles"),
19+
);
20+
21+
if (key) return attributes[key];
22+
return null;
23+
}
24+
1325
export function ResultViewer({ selectedResult }) {
1426
const { structureData, cifText } = useMemo(() => {
1527
if (!selectedResult) return { structureData: null, cifText: "" };
@@ -24,13 +36,29 @@ export function ResultViewer({ selectedResult }) {
2436
}
2537
}, [selectedResult]);
2638

39+
const crystalPositions = selectedResult?.attributes?.cartesian_site_positions;
40+
const smiles = getSmiles(selectedResult?.attributes);
41+
42+
const shouldRenderCrystal = structureData && crystalPositions;
43+
const shouldRenderSmiles = !shouldRenderCrystal && smiles;
44+
2745
return (
2846
<div className="w-full flex flex-col">
2947
{selectedResult ? (
3048
<div>
3149
<div className="@container w-full flex flex-col md:flex-row gap-2 md:gap-4">
3250
<div className="w-full md:w-1/2">
33-
<StructureViewerWithDownload OptimadeStructure={selectedResult} />
51+
{shouldRenderCrystal ? (
52+
<StructureViewerWithDownload
53+
OptimadeStructure={selectedResult}
54+
/>
55+
) : shouldRenderSmiles ? (
56+
<SmilesViewer smilesStr={smiles} width={450} height={450} />
57+
) : (
58+
<div className="text-center text-red-600">
59+
No crystal structure or SMILES available
60+
</div>
61+
)}
3462
</div>
3563
<div className={containerStyleHalf}>
3664
<JsonView
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { useEffect, useRef } from "react";
2+
import SmilesDrawer from "smiles-drawer";
3+
4+
import HelpIcon from "../common/HelpIcon";
5+
6+
const container = `w-full h-full flex border
7+
border-slate-500 rounded-sm
8+
items-center justify-center bg-slate-50 relative`;
9+
10+
export function SmilesViewer({
11+
smilesStr,
12+
type = "svg",
13+
width = 400,
14+
height = 400,
15+
}) {
16+
const containerRef = useRef(null);
17+
18+
useEffect(() => {
19+
if (!smilesStr || !containerRef.current) return;
20+
21+
const sd = new SmilesDrawer.SmiDrawer({
22+
width,
23+
height,
24+
padding: 15,
25+
bondThickness: 0.6,
26+
27+
fontSizeLarge: 6,
28+
});
29+
30+
sd.draw(smilesStr, containerRef.current);
31+
}, [smilesStr, width, height]);
32+
33+
if (type === "img") {
34+
return (
35+
<div className={container}>
36+
<img
37+
ref={containerRef}
38+
style={{ width, height }}
39+
alt="SMILES structure"
40+
/>
41+
</div>
42+
);
43+
}
44+
45+
return (
46+
// svg doesnt play that nice with the baseContainer so we hard code it here.
47+
<div className={container}>
48+
<div className="absolute top-2 right-2 z-10">
49+
<HelpIcon
50+
popover={`Smiles rendering is experimental and being done by SmilesDrawer 2.0, please report any issues you observe.`}
51+
placement="left"
52+
color="rgb(40,40,40)"
53+
/>
54+
</div>
55+
<svg ref={containerRef} width={width} height={height} />
56+
</div>
57+
);
58+
}

src/components/common/HelpIcon.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export default function HelpIcon({
5353
{popover && (
5454
<span
5555
className={`absolute ${positionClasses} w-max max-w-xs rounded-sm ${popoutBgColor} ${popoutTextColor} text-xs p-1
56-
text-center opacity-0 group-hover:opacity-100 transition-opacity z-50 pointer-events-none ${popoutStyleOverrides}`}
56+
text-center opacity-0 group-hover:opacity-100 transition-opacity duration-400 z-50 pointer-events-none ${popoutStyleOverrides}`}
5757
>
5858
{popover}
5959
</span>

0 commit comments

Comments
 (0)